Skip to content

fix(skills): let skill_manage modify skills in external_dirs in place#17512

Merged
teknium1 merged 1 commit into
mainfrom
hermes/hermes-0bff9892
Apr 29, 2026
Merged

fix(skills): let skill_manage modify skills in external_dirs in place#17512
teknium1 merged 1 commit into
mainfrom
hermes/hermes-0bff9892

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Closes #4759, closes #4381.

Summary

skill_manage patch/edit/write_file/remove_file/delete now work on skills that live under skills.external_dirs, editing in place instead of refusing with "external directory, copy it to your local skills first." The old gate is what caused the agent to fall back to action='create' and silently duplicate the external skill into ~/.hermes/skills/.

Root cause

  • _find_skill() already scans get_all_skills_dirs() (local + external), so lookup worked.
  • But every mutating action then gated on _is_local_skill() and refused.
  • Model's next move: action='create' with the same name → lands in ~/.hermes/skills/ because _create_skill always writes to local SKILLS_DIR. That's the duplicate copy users saw.

Changes

  • tools/skill_manager_tool.py: remove the read-only gate from _edit_skill, _patch_skill, _write_file, _remove_file, _delete_skill.
  • Replace _is_local_skill() with _containing_skills_root() so _delete_skill's empty-category cleanup bounds correctly at whichever root holds the skill (never rmdir's an external root).
  • _create_skill unchanged — new skills still land in local SKILLS_DIR only. Fewer moving parts than adding a default_write_dir config key.
  • 7 new TestExternalSkillMutations tests (patch/edit/write_file/remove_file/delete in place, empty-category cleanup, create-still-local).

Validation

Before After
patch/edit/write/delete an external skill error: "external directory … copy to local first" writes in place
Agent's fallback behavior creates silent duplicate in ~/.hermes/skills/ no duplicate
External root after deleting last skill n/a (gate blocked) root preserved, empty category cleaned
scripts/run_tests.sh tests/tools/test_skill_manager_tool.py tests/agent/test_external_skills.py ... 94 passed 101 passed (94 existing + 7 new)

E2E verified against a real HERMES_HOME with skills.external_dirs pointing at a temp vault dir — all six mutating operations wrote in place, local dir stayed empty, external vault root was never removed.

…rs in place

Closes #4759, closes #4381.

Mutating actions (patch, edit, write_file, remove_file, delete) used to
refuse skills that lived under `skills.external_dirs` with 'Skill X is in
an external directory and cannot be modified. Copy it to your local skills
directory first.'  Faced with that error, the agent would fall back to
action='create', which always writes under ~/.hermes/skills/ — producing
a silent duplicate of the external skill in the local store.

Fix: drop the read-only gate.  `skills.external_dirs` is configured by the
user; if they pointed it at a directory, they already said 'these are my
skills, treat them the same.'  Filesystem permissions handle the genuine
read-only case (write fails, agent sees the error).

- New _containing_skills_root() resolves whichever dir actually contains
  the skill; _delete_skill uses it to bound empty-category cleanup so an
  external root is never rmdir'd.
- _create_skill behavior is unchanged: new skills still land in local
  SKILLS_DIR only.  Fewer moving parts.
- Seven new TestExternalSkillMutations tests covering patch/edit/write_file/
  remove_file/delete/create against a mocked two-root layout + a category
  rmdir-safety check.
@teknium1 teknium1 merged commit 8c8fc6c into main Apr 29, 2026
11 of 12 checks passed
@teknium1 teknium1 deleted the hermes/hermes-0bff9892 branch April 29, 2026 15:16
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists tool/skills Skills system (list, view, manage) labels Apr 29, 2026
donald131 pushed a commit to donald131/hermes-agent that referenced this pull request May 2, 2026
…rs in place (NousResearch#17512)

Closes NousResearch#4759, closes NousResearch#4381.

Mutating actions (patch, edit, write_file, remove_file, delete) used to
refuse skills that lived under `skills.external_dirs` with 'Skill X is in
an external directory and cannot be modified. Copy it to your local skills
directory first.'  Faced with that error, the agent would fall back to
action='create', which always writes under ~/.hermes/skills/ — producing
a silent duplicate of the external skill in the local store.

Fix: drop the read-only gate.  `skills.external_dirs` is configured by the
user; if they pointed it at a directory, they already said 'these are my
skills, treat them the same.'  Filesystem permissions handle the genuine
read-only case (write fails, agent sees the error).

- New _containing_skills_root() resolves whichever dir actually contains
  the skill; _delete_skill uses it to bound empty-category cleanup so an
  external root is never rmdir'd.
- _create_skill behavior is unchanged: new skills still land in local
  SKILLS_DIR only.  Fewer moving parts.
- Seven new TestExternalSkillMutations tests covering patch/edit/write_file/
  remove_file/delete/create against a mocked two-root layout + a category
  rmdir-safety check.
02356abc pushed a commit to 02356abc/hermes-agent that referenced this pull request May 14, 2026
…rs in place (NousResearch#17512)

Closes NousResearch#4759, closes NousResearch#4381.

Mutating actions (patch, edit, write_file, remove_file, delete) used to
refuse skills that lived under `skills.external_dirs` with 'Skill X is in
an external directory and cannot be modified. Copy it to your local skills
directory first.'  Faced with that error, the agent would fall back to
action='create', which always writes under ~/.hermes/skills/ — producing
a silent duplicate of the external skill in the local store.

Fix: drop the read-only gate.  `skills.external_dirs` is configured by the
user; if they pointed it at a directory, they already said 'these are my
skills, treat them the same.'  Filesystem permissions handle the genuine
read-only case (write fails, agent sees the error).

- New _containing_skills_root() resolves whichever dir actually contains
  the skill; _delete_skill uses it to bound empty-category cleanup so an
  external root is never rmdir'd.
- _create_skill behavior is unchanged: new skills still land in local
  SKILLS_DIR only.  Fewer moving parts.
- Seven new TestExternalSkillMutations tests covering patch/edit/write_file/
  remove_file/delete/create against a mocked two-root layout + a category
  rmdir-safety check.
jsboige pushed a commit to jsboige/hermes-agent that referenced this pull request May 14, 2026
…rs in place (NousResearch#17512)

Closes NousResearch#4759, closes NousResearch#4381.

Mutating actions (patch, edit, write_file, remove_file, delete) used to
refuse skills that lived under `skills.external_dirs` with 'Skill X is in
an external directory and cannot be modified. Copy it to your local skills
directory first.'  Faced with that error, the agent would fall back to
action='create', which always writes under ~/.hermes/skills/ — producing
a silent duplicate of the external skill in the local store.

Fix: drop the read-only gate.  `skills.external_dirs` is configured by the
user; if they pointed it at a directory, they already said 'these are my
skills, treat them the same.'  Filesystem permissions handle the genuine
read-only case (write fails, agent sees the error).

- New _containing_skills_root() resolves whichever dir actually contains
  the skill; _delete_skill uses it to bound empty-category cleanup so an
  external root is never rmdir'd.
- _create_skill behavior is unchanged: new skills still land in local
  SKILLS_DIR only.  Fewer moving parts.
- Seven new TestExternalSkillMutations tests covering patch/edit/write_file/
  remove_file/delete/create against a mocked two-root layout + a category
  rmdir-safety check.
dannyJ848 pushed a commit to dannyJ848/hermes-agent that referenced this pull request May 17, 2026
…rs in place (NousResearch#17512)

Closes NousResearch#4759, closes NousResearch#4381.

Mutating actions (patch, edit, write_file, remove_file, delete) used to
refuse skills that lived under `skills.external_dirs` with 'Skill X is in
an external directory and cannot be modified. Copy it to your local skills
directory first.'  Faced with that error, the agent would fall back to
action='create', which always writes under ~/.hermes/skills/ — producing
a silent duplicate of the external skill in the local store.

Fix: drop the read-only gate.  `skills.external_dirs` is configured by the
user; if they pointed it at a directory, they already said 'these are my
skills, treat them the same.'  Filesystem permissions handle the genuine
read-only case (write fails, agent sees the error).

- New _containing_skills_root() resolves whichever dir actually contains
  the skill; _delete_skill uses it to bound empty-category cleanup so an
  external root is never rmdir'd.
- _create_skill behavior is unchanged: new skills still land in local
  SKILLS_DIR only.  Fewer moving parts.
- Seven new TestExternalSkillMutations tests covering patch/edit/write_file/
  remove_file/delete/create against a mocked two-root layout + a category
  rmdir-safety check.
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
…rs in place (NousResearch#17512)

Closes NousResearch#4759, closes NousResearch#4381.

Mutating actions (patch, edit, write_file, remove_file, delete) used to
refuse skills that lived under `skills.external_dirs` with 'Skill X is in
an external directory and cannot be modified. Copy it to your local skills
directory first.'  Faced with that error, the agent would fall back to
action='create', which always writes under ~/.hermes/skills/ — producing
a silent duplicate of the external skill in the local store.

Fix: drop the read-only gate.  `skills.external_dirs` is configured by the
user; if they pointed it at a directory, they already said 'these are my
skills, treat them the same.'  Filesystem permissions handle the genuine
read-only case (write fails, agent sees the error).

- New _containing_skills_root() resolves whichever dir actually contains
  the skill; _delete_skill uses it to bound empty-category cleanup so an
  external root is never rmdir'd.
- _create_skill behavior is unchanged: new skills still land in local
  SKILLS_DIR only.  Fewer moving parts.
- Seven new TestExternalSkillMutations tests covering patch/edit/write_file/
  remove_file/delete/create against a mocked two-root layout + a category
  rmdir-safety check.
mbs-vhs added a commit to mbs-vhs/hermes-agent that referenced this pull request Jun 4, 2026
…AWD-1110)

The background skill-review fork can write into live dev repos that
operators expose via skills.external_dirs (the 2026-06-03 incident:
bg-review wrote references/ files + patched SKILL.md directly under
~/dev/devops-process/skills, exposed via external_dirs on all 10
profiles).

Add skills.external_dirs_readonly (default True). When on, the five
skill_manage mutating actions (edit/patch/write_file/remove_file/delete)
refuse any skill whose resolved directory lies under a skills.external_dirs
entry, returning an error WITHOUT writing. Reads/listing/consumption and
create (always local-only) are unaffected. Set the flag false to restore
the legacy in-place behavior (PR NousResearch#17512).

The gate is a single chokepoint (_assert_writable) called after _find_skill
resolves the target; realpaths are compared so a symlinked external dir
can't slip past. Default reader mirrors _guard_agent_created_enabled but
fails safe-ON.

Tests: new TestExternalDirsReadonlyGate + TestExternalDirsReadonlyFlag
classes (block on external / allow on local / create stays local / flag
false = legacy / symlink-robust / config default-true). Existing
TestExternalSkillMutations now pins the flag off to keep covering the
legacy path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Egavasyug pushed a commit to Egavasyug/hermes-agent that referenced this pull request Jun 10, 2026
…rs in place (NousResearch#17512)

Closes NousResearch#4759, closes NousResearch#4381.

Mutating actions (patch, edit, write_file, remove_file, delete) used to
refuse skills that lived under `skills.external_dirs` with 'Skill X is in
an external directory and cannot be modified. Copy it to your local skills
directory first.'  Faced with that error, the agent would fall back to
action='create', which always writes under ~/.hermes/skills/ — producing
a silent duplicate of the external skill in the local store.

Fix: drop the read-only gate.  `skills.external_dirs` is configured by the
user; if they pointed it at a directory, they already said 'these are my
skills, treat them the same.'  Filesystem permissions handle the genuine
read-only case (write fails, agent sees the error).

- New _containing_skills_root() resolves whichever dir actually contains
  the skill; _delete_skill uses it to bound empty-category cleanup so an
  external root is never rmdir'd.
- _create_skill behavior is unchanged: new skills still land in local
  SKILLS_DIR only.  Fewer moving parts.
- Seven new TestExternalSkillMutations tests covering patch/edit/write_file/
  remove_file/delete/create against a mocked two-root layout + a category
  rmdir-safety check.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Medium — degraded but workaround exists tool/skills Skills system (list, view, manage) type/bug Something isn't working

Projects

None yet

2 participants