Skip to content

Close three escapes in the Claude tool gate - #8

Merged
sergeyzenchenko merged 1 commit into
techery:mainfrom
ASemeniuk:fix/tool-gate-escapes
Aug 27, 2026
Merged

Close three escapes in the Claude tool gate#8
sergeyzenchenko merged 1 commit into
techery:mainfrom
ASemeniuk:fix/tool-gate-escapes

Conversation

@ASemeniuk

Copy link
Copy Markdown
Contributor

Three ways an agent step could write outside the boundary createToolGate is supposed to enforce. Each fix is paired with a test that fails without it (verified by reverting gate.ts alone: exactly three failures, zero with the fix).

Found while evaluating Weft — a review run over its own sources surfaced the first two; the third came out of reading the surrounding screens.

1. A .. segment after a symlink smuggled an edit out of the worktree

resolvesOutsideWorktree collapses the path with path.resolve before it probes the filesystem. With sub/esc a committed symlink to an outside directory, sub/esc/../stolen becomes sub/stolen — the symlink is erased from the probe, realpath on the surviving ancestor reports in-tree, and the function returns false. POSIX resolves left to right, so the write actually lands in the symlink target's parent, invisible to patch capture.

The shell surface already refuses .. outright via PARENT_TRAVERSAL, with a comment explaining that a false deny beats an unquarantined write. The edit surface applied no .. screen at all. This applies the same coarse screen there, under a strict scope or a task context.

2. The .weft/tasks guard was tested against the raw tool argument

The regex ran on target (the argument verbatim, per editTargetPath), so .weft/foo/../tasks/x.json and .weft//tasks/x.json missed it — and then workspacePath normalized them straight back onto the store two lines later. A task-aware agent could edit the engine-owned store directly, bypassing the taskOperations settlement channel.

Now the normalized path is screened as well. The test asserts three spellings of the same file, all denied.

3. An absolute destination attached to its redirection escaped both strict-scope screens

printf x >/etc/cron.d/pwn passed everything:

  • OUT_OF_TREE_PATH requires the / to follow start-of-string, whitespace, = or a quote — > is not in the class;
  • commandEscapesWorktree strips the redirection operator and then continues on any token starting with /.

So the command was never denied up front as the comment above it documents, and instead reached hitl.onPermission, where an auto-approving policy — the case that comment anticipates — is the only thing left. Defense-in-depth miss rather than a silent allow, but the strict scope's stated guarantee was gone.

Fixed by adding the shell's own operators to the leading class. : is deliberately left out so ordinary URLs (curl http://host/p) still pass; the test pins that.

Verification

  • vitest run — 923 passed, 1 skipped, 53 files
  • pnpm typecheck — clean
  • biome check on both changed files — clean

pnpm lint is red on main for seven pre-existing findings in apps/ui, packages/design-system, packages/gate and packages/host; untouched here to keep the diff to the boundary.

Each is reproduced by a test that fails without the corresponding fix.

1. A `..` segment after a symlink smuggled an edit out of the worktree.
   `resolvesOutsideWorktree` collapses the path with `path.resolve` before it
   probes the filesystem, so `sub/esc/../stolen` (where `sub/esc` is a committed
   symlink) becomes `sub/stolen` — the symlink is erased from the probe and the
   write reads as in-tree, while POSIX resolves left to right and lands it in the
   symlink target's parent. The shell surface already refuses `..` outright via
   PARENT_TRAVERSAL; the edit surface applied no `..` screen at all. Apply the
   same coarse screen there under a strict scope or a task context.

2. The engine-owned `.weft/tasks` guard tested the RAW tool argument, so any
   non-canonical spelling of the same file (`.weft/foo/../tasks/x`,
   `.weft//tasks/x`) missed the regex and then normalized straight back onto the
   store two lines later. Screen the normalized `workspacePath` as well.

3. An absolute destination ATTACHED to its redirection (`>/etc/x`, `2>>/etc/x`)
   escaped both strict-scope screens: `OUT_OF_TREE_PATH` required the `/` to
   follow start-of-string, whitespace, `=` or a quote, and `commandEscapesWorktree`
   strips the redirection operator and then skips any token starting with `/`. The
   command reached the approval broker instead of being denied up front, so an
   auto-approving policy was the only thing left. Add the shell's own operators to
   the leading class; `:` stays out so `curl http://host/p` still passes.
@sergeyzenchenko
sergeyzenchenko merged commit 6101f0a into techery:main Aug 27, 2026
4 of 6 checks passed
@ASemeniuk
ASemeniuk deleted the fix/tool-gate-escapes branch August 27, 2026 12:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants