Skip to content

fix(skills): guard recursive skill delete against tree-escape (port Kilo #11240) - #46929

Merged
teknium1 merged 1 commit into
mainfrom
kilocode-port/skill-delete-rmtree-guard
Jun 16, 2026
Merged

fix(skills): guard recursive skill delete against tree-escape (port Kilo #11240)#46929
teknium1 merged 1 commit into
mainfrom
kilocode-port/skill-delete-rmtree-guard

Conversation

@teknium1

@teknium1 teknium1 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Summary

The agent-facing skill_manage(action='delete') path now refuses to recursively delete a skill directory that could escape the skills tree. Previously it called shutil.rmtree(skill_dir) with no last-line validation.

Ported from Kilo-Org/kilocode#11240. Their issue #11227 caused complete loss of a user's working directory: a built-in-skill sentinel location resolved to the server cwd, and the skill-removal endpoint ran a recursive delete on path.dirname() of it.

What was already safe (and what wasn't)

  • /skills uninstall (tools/skills_hub.py) is already hardened — it has _normalize_lock_install_path, symlink/junction redirect rejection, and resolve-then-reject-root checks. No change there.
  • The agent-facing skill_manage(action='delete') path in tools/skill_manager_tool.py had none of that. skill_dir comes from _find_skill (a real SKILL.md parent discovered by walking the roots), so arbitrary-path injection isn't directly reachable today — but the bare rmtree had no defense-in-depth against a poisoned/symlinked skills tree or a discovery edge case handing back a root.

Changes

  • tools/skill_manager_tool.py: add _validate_delete_target() (+ _is_path_redirect helper) called before shutil.rmtree in _delete_skill. Refuses when the skill dir (1) doesn't resolve strictly inside a known skills root, (2) is a skills root (would wipe every installed skill), or (3) is reached via a symlink/junction (rmtree would follow it out of tree).
  • tests/tools/test_skill_manager_tool.py: new TestDeleteSkillRmtreeGuard (4 cases).

How it was adapted

Kilo's fix is TypeScript/Effect over an HTTP endpoint that accepted a client-supplied location string; the core risk was arbitrary path → resolve → dirname → recursive rm. Hermes doesn't take a client path here (discovery-only), so this is the matching defense-in-depth for the recursive-delete primitive rather than a 1:1 port. The symlink/junction check matches the existing _is_path_redirect pattern already used in skills_hub.py.

Validation

Before After
Normal delete works works
Symlinked skill dir rmtree follows link, deletes target contents refused, target intact
Skills root handed to delete wipes all skills refused
Out-of-tree absolute path deletes it refused

tests/tools/test_skill_manager_tool.py: 94 passed (90 prior + 4 new). E2E verified with real symlink creation + file I/O against a temp HERMES_HOME.

Infographic

skill-delete-tree-escape-guard

Port from Kilo-Org/kilocode#11240. Their issue #11227 lost a user's entire
working directory: a built-in-skill sentinel location resolved to the server
cwd and the skill-removal endpoint ran a recursive delete on it.

Hermes' /skills uninstall path (skills_hub.py) is already hardened, but the
agent-facing skill_manage(action='delete') path did a bare
shutil.rmtree(skill_dir) with no last-line validation. Add _validate_delete_target():
refuse to rmtree a path that (1) isn't strictly inside a known skills root,
(2) is a skills root itself, or (3) is reached via a symlink/junction.

Tests: 4 cases (normal delete works; symlinked dir, skills-root, out-of-tree
all refused). E2E verified with real symlink + file I/O.
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Lint report: kilocode-port/skill-delete-rmtree-guard vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 10962 on HEAD, 10960 on base (🆕 +2)

🆕 New issues (2):

Rule Count
unresolved-attribute 2
First entries
run_agent.py:2920: [unresolved-attribute] unresolved-attribute: Object of type `Self@get_credits_spent_micros` has no attribute `_credits_session_start_micros`
tests/run_agent/test_credits_notices_toggle.py:76: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_credits_session_start_micros` on type `AIAgent`

✅ Fixed issues (1):

Rule Count
invalid-assignment 1
First entries
tests/run_agent/test_credits_notices_toggle.py:76: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to attribute `_credits_session_start_micros` of type `int`

Unchanged: 5771 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/security Security vulnerability or hardening comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint tool/skills Skills system (list, view, manage) P2 Medium — degraded but workaround exists labels Jun 16, 2026
@teknium1
teknium1 merged commit 2dbc3bd into main Jun 16, 2026
35 checks passed
@teknium1
teknium1 deleted the kilocode-port/skill-delete-rmtree-guard branch June 16, 2026 00:15
alanbratu pushed a commit to alanbratu/hermes-agent that referenced this pull request Jun 17, 2026
…search#46929)

Port from Kilo-Org/kilocode#11240. Their issue NousResearch#11227 lost a user's entire
working directory: a built-in-skill sentinel location resolved to the server
cwd and the skill-removal endpoint ran a recursive delete on it.

Hermes' /skills uninstall path (skills_hub.py) is already hardened, but the
agent-facing skill_manage(action='delete') path did a bare
shutil.rmtree(skill_dir) with no last-line validation. Add _validate_delete_target():
refuse to rmtree a path that (1) isn't strictly inside a known skills root,
(2) is a skills root itself, or (3) is reached via a symlink/junction.

Tests: 4 cases (normal delete works; symlinked dir, skills-root, out-of-tree
all refused). E2E verified with real symlink + file I/O.
zmlgit pushed a commit to zmlgit/hermes-agent that referenced this pull request Jun 17, 2026
…search#46929)

Port from Kilo-Org/kilocode#11240. Their issue NousResearch#11227 lost a user's entire
working directory: a built-in-skill sentinel location resolved to the server
cwd and the skill-removal endpoint ran a recursive delete on it.

Hermes' /skills uninstall path (skills_hub.py) is already hardened, but the
agent-facing skill_manage(action='delete') path did a bare
shutil.rmtree(skill_dir) with no last-line validation. Add _validate_delete_target():
refuse to rmtree a path that (1) isn't strictly inside a known skills root,
(2) is a skills root itself, or (3) is reached via a symlink/junction.

Tests: 4 cases (normal delete works; symlinked dir, skills-root, out-of-tree
all refused). E2E verified with real symlink + file I/O.
al3xar pushed a commit to al3xar/hermes-agent that referenced this pull request Jun 17, 2026
…search#46929)

Port from Kilo-Org/kilocode#11240. Their issue NousResearch#11227 lost a user's entire
working directory: a built-in-skill sentinel location resolved to the server
cwd and the skill-removal endpoint ran a recursive delete on it.

Hermes' /skills uninstall path (skills_hub.py) is already hardened, but the
agent-facing skill_manage(action='delete') path did a bare
shutil.rmtree(skill_dir) with no last-line validation. Add _validate_delete_target():
refuse to rmtree a path that (1) isn't strictly inside a known skills root,
(2) is a skills root itself, or (3) is reached via a symlink/junction.

Tests: 4 cases (normal delete works; symlinked dir, skills-root, out-of-tree
all refused). E2E verified with real symlink + file I/O.
T02200059 pushed a commit to T02200059/hermes-agent that referenced this pull request Jun 18, 2026
…search#46929)

Port from Kilo-Org/kilocode#11240. Their issue NousResearch#11227 lost a user's entire
working directory: a built-in-skill sentinel location resolved to the server
cwd and the skill-removal endpoint ran a recursive delete on it.

Hermes' /skills uninstall path (skills_hub.py) is already hardened, but the
agent-facing skill_manage(action='delete') path did a bare
shutil.rmtree(skill_dir) with no last-line validation. Add _validate_delete_target():
refuse to rmtree a path that (1) isn't strictly inside a known skills root,
(2) is a skills root itself, or (3) is reached via a symlink/junction.

Tests: 4 cases (normal delete works; symlinked dir, skills-root, out-of-tree
all refused). E2E verified with real symlink + file I/O.
binsonzhang95-maker pushed a commit to binsonzhang95-maker/hermes-agent that referenced this pull request Jun 20, 2026
…search#46929)

Port from Kilo-Org/kilocode#11240. Their issue NousResearch#11227 lost a user's entire
working directory: a built-in-skill sentinel location resolved to the server
cwd and the skill-removal endpoint ran a recursive delete on it.

Hermes' /skills uninstall path (skills_hub.py) is already hardened, but the
agent-facing skill_manage(action='delete') path did a bare
shutil.rmtree(skill_dir) with no last-line validation. Add _validate_delete_target():
refuse to rmtree a path that (1) isn't strictly inside a known skills root,
(2) is a skills root itself, or (3) is reached via a symlink/junction.

Tests: 4 cases (normal delete works; symlinked dir, skills-root, out-of-tree
all refused). E2E verified with real symlink + file I/O.

(cherry picked from commit 2dbc3bd)
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…search#46929)

Port from Kilo-Org/kilocode#11240. Their issue NousResearch#11227 lost a user's entire
working directory: a built-in-skill sentinel location resolved to the server
cwd and the skill-removal endpoint ran a recursive delete on it.

Hermes' /skills uninstall path (skills_hub.py) is already hardened, but the
agent-facing skill_manage(action='delete') path did a bare
shutil.rmtree(skill_dir) with no last-line validation. Add _validate_delete_target():
refuse to rmtree a path that (1) isn't strictly inside a known skills root,
(2) is a skills root itself, or (3) is reached via a symlink/junction.

Tests: 4 cases (normal delete works; symlinked dir, skills-root, out-of-tree
all refused). E2E verified with real symlink + file I/O.
Methodician added a commit to Methodician/hermes-agent that referenced this pull request Jul 4, 2026
…search#46929)

Port from Kilo-Org/kilocode#11240. Their issue NousResearch#11227 lost a user's entire
working directory: a built-in-skill sentinel location resolved to the server
cwd and the skill-removal endpoint ran a recursive delete on it.

Hermes' /skills uninstall path (skills_hub.py) is already hardened, but the
agent-facing skill_manage(action='delete') path did a bare
shutil.rmtree(skill_dir) with no last-line validation. Add _validate_delete_target():
refuse to rmtree a path that (1) isn't strictly inside a known skills root,
(2) is a skills root itself, or (3) is reached via a symlink/junction.

Tests: 4 cases (normal delete works; symlinked dir, skills-root, out-of-tree
all refused). E2E verified with real symlink + file I/O.
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
…search#46929)

Port from Kilo-Org/kilocode#11240. Their issue NousResearch#11227 lost a user's entire
working directory: a built-in-skill sentinel location resolved to the server
cwd and the skill-removal endpoint ran a recursive delete on it.

Hermes' /skills uninstall path (skills_hub.py) is already hardened, but the
agent-facing skill_manage(action='delete') path did a bare
shutil.rmtree(skill_dir) with no last-line validation. Add _validate_delete_target():
refuse to rmtree a path that (1) isn't strictly inside a known skills root,
(2) is a skills root itself, or (3) is reached via a symlink/junction.

Tests: 4 cases (normal delete works; symlinked dir, skills-root, out-of-tree
all refused). E2E verified with real symlink + file I/O.
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
…search#46929)

Port from Kilo-Org/kilocode#11240. Their issue NousResearch#11227 lost a user's entire
working directory: a built-in-skill sentinel location resolved to the server
cwd and the skill-removal endpoint ran a recursive delete on it.

Hermes' /skills uninstall path (skills_hub.py) is already hardened, but the
agent-facing skill_manage(action='delete') path did a bare
shutil.rmtree(skill_dir) with no last-line validation. Add _validate_delete_target():
refuse to rmtree a path that (1) isn't strictly inside a known skills root,
(2) is a skills root itself, or (3) is reached via a symlink/junction.

Tests: 4 cases (normal delete works; symlinked dir, skills-root, out-of-tree
all refused). E2E verified with real symlink + file I/O.
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…search#46929)

Port from Kilo-Org/kilocode#11240. Their issue NousResearch#11227 lost a user's entire
working directory: a built-in-skill sentinel location resolved to the server
cwd and the skill-removal endpoint ran a recursive delete on it.

Hermes' /skills uninstall path (skills_hub.py) is already hardened, but the
agent-facing skill_manage(action='delete') path did a bare
shutil.rmtree(skill_dir) with no last-line validation. Add _validate_delete_target():
refuse to rmtree a path that (1) isn't strictly inside a known skills root,
(2) is a skills root itself, or (3) is reached via a symlink/junction.

Tests: 4 cases (normal delete works; symlinked dir, skills-root, out-of-tree
all refused). E2E verified with real symlink + file I/O.
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
…search#46929)

Port from Kilo-Org/kilocode#11240. Their issue NousResearch#11227 lost a user's entire
working directory: a built-in-skill sentinel location resolved to the server
cwd and the skill-removal endpoint ran a recursive delete on it.

Hermes' /skills uninstall path (skills_hub.py) is already hardened, but the
agent-facing skill_manage(action='delete') path did a bare
shutil.rmtree(skill_dir) with no last-line validation. Add _validate_delete_target():
refuse to rmtree a path that (1) isn't strictly inside a known skills root,
(2) is a skills root itself, or (3) is reached via a symlink/junction.

Tests: 4 cases (normal delete works; symlinked dir, skills-root, out-of-tree
all refused). E2E verified with real symlink + file I/O.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists tool/skills Skills system (list, view, manage) type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants