Skip to content

fix(skills): make Nix store skill copies writable - #20135

Closed
haoxianhan wants to merge 1 commit into
NousResearch:mainfrom
haoxianhan:fix/skills-copy-nix-store-permissions
Closed

fix(skills): make Nix store skill copies writable#20135
haoxianhan wants to merge 1 commit into
NousResearch:mainfrom
haoxianhan:fix/skills-copy-nix-store-permissions

Conversation

@haoxianhan

@haoxianhan haoxianhan commented May 5, 2026

Copy link
Copy Markdown

Summary

  • make bundled skill files copied from Nix store paths owner-writable without dropping existing mode bits, including executable scripts
  • make copied skill directories owner-writable after copytree reapplies source directory metadata
  • cover read-only files, executable files, read-only directories, and end-to-end sync from a read-only bundled skill tree

Root cause

Nix store paths are read-only. shutil.copy2 preserves file modes, and shutil.copytree also reapplies directory metadata after copying. When HERMES_BUNDLED_SKILLS points at a Nix-managed bundled skills tree, synced copies under ~/.hermes/skills could inherit unwritable files or directories.

Validation

  • python3 -m py_compile tools/skills_sync.py tests/tools/test_skills_sync.py
  • minimal local permission reproduction for 0555 directories and executable files

Full pytest was not run in this checkout because no project virtualenv was available.

@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have area/nix Nix flake, NixOS module, container packaging tool/skills Skills system (list, view, manage) labels May 5, 2026
Bundled skills copied from Nix-managed HERMES_BUNDLED_SKILLS paths inherit read-only modes from /nix/store. shutil.copy2 preserves read-only file modes, and copytree also reapplies directory metadata after copying, so ~/.hermes/skills copies can end up with unwritable files or directories.

Ensure copied files and directories gain owner write permission while preserving existing mode bits such as executable scripts. Use the writable tree copy path for both initial bundled skill syncs and bundled skill updates.

Add regression coverage for read-only files, executable files, read-only directories, and end-to-end sync from a read-only bundled skill tree.
@haoxianhan
haoxianhan force-pushed the fix/skills-copy-nix-store-permissions branch from 3ce4773 to 1648bfb Compare May 13, 2026 01:30
@haoxianhan haoxianhan changed the title fix(skills): strip read-only bits when copying from Nix store fix(skills): make Nix store skill copies writable May 13, 2026
@haoxianhan haoxianhan closed this May 21, 2026
ethernet8023 pushed a commit that referenced this pull request Aug 3, 2026
shutil.copy2 and shutil.copytree both preserve source-file mode bits.
When bundled skills are sourced from a read-only filesystem — the Nix
store (mode 0444/0555), squashfs, or an OCI image layer — the user copy
in ~/.hermes/skills/ inherits those bits. Later edits via skill_manage,
the curator, or even the shutil.rmtree(*.bak) cleanup at the end of an
update then fail with PermissionError (silently swallowed by
ignore_errors=True, so stale *.bak directories accumulate).

Three helpers fix the root cause at the copy boundary:

- _ensure_owner_writable(path) — adds the owner-write bit on a single
  file or directory without touching other mode bits (skips symlinks:
  os.chmod follows them by default, and a copied symlink pointing
  outside the profile/tree must not have its external target mutated).
- _copy_file_writable(src, dst) — drop-in shutil.copy2 replacement,
  also used as copytree's copy_function.
- _copytree_writable(src, dst) — drop-in shutil.copytree replacement;
  sweeps the destination tree afterwards since copytree reapplies
  source-directory metadata after file copies.

Wired into every copy site that may read from a read-only bundled
source: tools/skills_sync.py (sync_skills new + update paths,
restore_official_optional_skill, the DESCRIPTION.md copy, and the
reset_bundled_skill rmtree), hermes_cli/profiles.py (--clone and
--clone-all), and hermes_cli/profile_distribution.py
(apply_distribution).

Also repairs two states the copy-boundary fix alone doesn't reach:

- Migration for existing installs: a user copy that predates this fix
  can be hash-identical to the bundled source, so sync_skills takes
  the "unchanged" no-op path and would never repair it. Both the
  v1-migration branch and the "bundled unchanged, user unchanged"
  branch now sweep _make_tree_owner_writable(dest) unconditionally.
- Symlink safety in the profile-clone repair sweep: profiles.py clones
  skills with shutil.copytree(..., symlinks=True, ...), so a skill
  that is itself a symlink to a shared/vendored directory outside the
  profile reaches the writable-mode repair as a real symlink entry.
  _ensure_owner_writable skips symlinks rather than chmod-ing through
  them into whatever they still point at.

Salvages closed PR #20135 (closed by author, not maintainer-rejected)
and extends its coverage. Complements #34860 (83a7d0b / 8ae0802),
which fixed the removal side (_rmtree_writable making read-only trees
removable) — this fixes the copy side (preventing read-only trees from
being created in the first place).
legacynode added a commit to legacycode-forks/hermes-agent that referenced this pull request Aug 17, 2026
shutil.copy2 and shutil.copytree both preserve source-file mode bits.
When bundled skills are sourced from a read-only filesystem — the Nix
store (mode 0444/0555), squashfs, or an OCI image layer — the user copy
in ~/.hermes/skills/ inherits those bits. Later edits via skill_manage,
the curator, or even the shutil.rmtree(*.bak) cleanup at the end of an
update then fail with PermissionError (silently swallowed by
ignore_errors=True, so stale *.bak directories accumulate).

Three helpers fix the root cause at the copy boundary:

- _ensure_owner_writable(path) — adds the owner-write bit on a single
  file or directory without touching other mode bits (skips symlinks:
  os.chmod follows them by default, and a copied symlink pointing
  outside the profile/tree must not have its external target mutated).
- _copy_file_writable(src, dst) — drop-in shutil.copy2 replacement,
  also used as copytree's copy_function.
- _copytree_writable(src, dst) — drop-in shutil.copytree replacement;
  sweeps the destination tree afterwards since copytree reapplies
  source-directory metadata after file copies.

Wired into every copy site that may read from a read-only bundled
source: tools/skills_sync.py (sync_skills new + update paths,
restore_official_optional_skill, the DESCRIPTION.md copy, and the
reset_bundled_skill rmtree), hermes_cli/profiles.py (--clone and
--clone-all), and hermes_cli/profile_distribution.py
(apply_distribution).

Also repairs two states the copy-boundary fix alone doesn't reach:

- Migration for existing installs: a user copy that predates this fix
  can be hash-identical to the bundled source, so sync_skills takes
  the "unchanged" no-op path and would never repair it. Both the
  v1-migration branch and the "bundled unchanged, user unchanged"
  branch now sweep _make_tree_owner_writable(dest) unconditionally.
- Symlink safety in the profile-clone repair sweep: profiles.py clones
  skills with shutil.copytree(..., symlinks=True, ...), so a skill
  that is itself a symlink to a shared/vendored directory outside the
  profile reaches the writable-mode repair as a real symlink entry.
  _ensure_owner_writable skips symlinks rather than chmod-ing through
  them into whatever they still point at.

Salvages closed PR NousResearch#20135 (closed by author, not maintainer-rejected)
and extends its coverage. Complements NousResearch#34860 (83a7d0b / 8ae0802),
which fixed the removal side (_rmtree_writable making read-only trees
removable) — this fixes the copy side (preventing read-only trees from
being created in the first place).
jh1nresh pushed a commit to jh1nresh/hermes-agent that referenced this pull request Aug 25, 2026
shutil.copy2 and shutil.copytree both preserve source-file mode bits.
When bundled skills are sourced from a read-only filesystem — the Nix
store (mode 0444/0555), squashfs, or an OCI image layer — the user copy
in ~/.hermes/skills/ inherits those bits. Later edits via skill_manage,
the curator, or even the shutil.rmtree(*.bak) cleanup at the end of an
update then fail with PermissionError (silently swallowed by
ignore_errors=True, so stale *.bak directories accumulate).

Three helpers fix the root cause at the copy boundary:

- _ensure_owner_writable(path) — adds the owner-write bit on a single
  file or directory without touching other mode bits (skips symlinks:
  os.chmod follows them by default, and a copied symlink pointing
  outside the profile/tree must not have its external target mutated).
- _copy_file_writable(src, dst) — drop-in shutil.copy2 replacement,
  also used as copytree's copy_function.
- _copytree_writable(src, dst) — drop-in shutil.copytree replacement;
  sweeps the destination tree afterwards since copytree reapplies
  source-directory metadata after file copies.

Wired into every copy site that may read from a read-only bundled
source: tools/skills_sync.py (sync_skills new + update paths,
restore_official_optional_skill, the DESCRIPTION.md copy, and the
reset_bundled_skill rmtree), hermes_cli/profiles.py (--clone and
--clone-all), and hermes_cli/profile_distribution.py
(apply_distribution).

Also repairs two states the copy-boundary fix alone doesn't reach:

- Migration for existing installs: a user copy that predates this fix
  can be hash-identical to the bundled source, so sync_skills takes
  the "unchanged" no-op path and would never repair it. Both the
  v1-migration branch and the "bundled unchanged, user unchanged"
  branch now sweep _make_tree_owner_writable(dest) unconditionally.
- Symlink safety in the profile-clone repair sweep: profiles.py clones
  skills with shutil.copytree(..., symlinks=True, ...), so a skill
  that is itself a symlink to a shared/vendored directory outside the
  profile reaches the writable-mode repair as a real symlink entry.
  _ensure_owner_writable skips symlinks rather than chmod-ing through
  them into whatever they still point at.

Salvages closed PR NousResearch#20135 (closed by author, not maintainer-rejected)
and extends its coverage. Complements NousResearch#34860 (83a7d0b / 8ae0802),
which fixed the removal side (_rmtree_writable making read-only trees
removable) — this fixes the copy side (preventing read-only trees from
being created in the first place).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/nix Nix flake, NixOS module, container packaging P3 Low — cosmetic, nice to have tool/skills Skills system (list, view, manage) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants