Skip to content

Handoff example: stricter, honestly described approval gate; timings including the policy; demo command - #297

Merged
yuanhao merged 2 commits into
mainfrom
fix/handoff-review
Oct 11, 2026
Merged

yuanhao merged 2 commits into
mainfrom
fix/handoff-review

Conversation

@yuanhao

@yuanhao yuanhao commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes from a review of the handoff example's README (#296).

[P1] The safety claim was too absolute, and the gate had holes. These four went through without approval:

Probe Why it passed
/bin/rm -f owner.txt the program was compared by full path
git restore owner.txt only -- or . were recognized
git -C . reset --hard git's global options broke the pattern
printf changed 1>owner.txt a digit before > was treated as stderr-only

confirm-dangerous.ts now parses each command:

  • the program's base name, past env / command / nohup / time / VAR=…;
  • git's subcommand, past -C / -c / --git-dir;
  • every truncating redirection (>, 1>, 2>, &>; not >>, nor /dev/null);
  • tee without -a;
  • checkout / restore of paths (not restore --staged);
  • forced and mirror pushes, and stash drop / clear.

I checked 45 cases: the four probes; 22 more that must ask; 14 everyday commands that must stay free (>>, 2>/dev/null, git checkout -b, git restore --staged, a plain push…); and 5 writes and edits.

The docs (example README, frontend README, CHANGELOG, the example's header) now call it approval guardrails for selected operations, not a sandbox. Edits of existing files, and what a script does inside, are not checked. For a guarantee, use a container or a disposable copy.

[P2] The timings left out the policy. ToolExecutionStart is sent after the before_tool chain (agent_loop.rs:2545 vs :2568), so the per-call numbers missed the pi policy round trip, and the "0.1%" didn't follow. The demo now reports two parts: loop + policy (from the model message's end to the tool's start) and tool. Release build, M2, 5 runs:

Loop + policy Tool
first pi call of a session 9–11 ms ~5.5 ms
later calls ~3 ms (1.4–5.7) 0.6–12 ms

The percentage is gone.

[P2] The demo command. cargo run --example handoff_agent from a project directory can't find the crate. The docs now use scripts/try-handoff.sh --demo, and the example's header says --manifest-path is needed for cargo run there.

Checks: all 41 frontend tests pass, run twice. coding_agent's CI check (? Run \rm -rf build`?, bash ✗`) holds, and lint is clean.

Second round (review of this PR)

The first rewrite still split commands with regexes, and a review found concrete bypasses. The gate now tokenizes each command (quotes, escapes, redirections incl. >|; heredoc bodies skipped) and follows cd. Before, cd sub && echo x > only.txt was checked against the wrong folder.

  • Unknown paths ask: $VAR, ~, globs, an unclosed quote.
  • Now covered: bash -c / sh -c (read recursively), xargs rm, find -exec rm, git rm, the wrappers timeout / nice / env -i, dd of=, +ref / :ref pushes, checkout -f / switch -f.
  • Fewer false refusals: cargo test truncate, a commit message mentioning sudo, git clean -n, heredoc bodies, and repeated tee test.log / cp are free.
  • Bookkeeping: a refused command's files are no longer counted as the agent's.
  • confirm-dangerous.test.mjs: 70 cases, run in CI.
  • README: the timing column is now "tool + after-tool hooks", with the concurrent-calls caveat.

🤖 Generated with Claude Code

https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG

yuanhao and others added 2 commits October 11, 2026 15:44
…timings that include the policy; working demo command

From review of the handoff example's README:
- [P1] "It cannot do damage" overstated the gate, and probes went through:
  `/bin/rm -f owner.txt`, `git restore owner.txt`, `git -C . reset --hard`,
  `printf changed 1>owner.txt`. confirm-dangerous.ts now parses each command
  (the program's base name past env/command/nohup/time/VAR=…; git's
  subcommand past -C/-c/--git-dir…; every truncating redirection `>`, `1>`,
  `2>`, `&>`, not `>>` or to /dev/null; `tee` without -a; `git checkout`
  / `restore` of paths, not `restore --staged`; forced and mirror pushes;
  `stash drop` / `clear`). Checked against 45 commands and writes. The docs
  call it a guardrail for selected operations, not a sandbox: edits of
  existing files and what a script does inside are not checked.
- [P2] The per-call timings started at ToolExecutionStart, which comes
  after the before_tool policies, so they left out the pi policy round
  trip; the "0.1%" did not follow. The demo now reports "loop + policy"
  (model message end → tool start) and the tool apart: ~3 ms + 0.6–12 ms
  warm, 9–11 ms + 5.5 ms for a session's first pi call. The percentage is
  gone.
- [P2] `cargo run --example handoff_agent` from a project directory cannot
  find the crate: the docs use `scripts/try-handoff.sh --demo` (and say
  `--manifest-path` is needed for cargo run there).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
…has tests

From the review of #297:
- A real tokenizer (quotes, escapes, operators, redirections incl. `>|`,
  `&>`, `N>`; heredoc bodies skipped) and a running cwd: `cd sub && echo x >
  only.txt` was checked against the wrong directory; an unfollowable `cd`
  makes later paths unknown.
- Unknown paths ask: `$VAR`, `~`, backquotes, globs, an unclosed quote
  (`echo x > ~/.zshrc`, `> "my file.txt"` passed).
- More covered: `bash -c` / `sh -c` scripts (read the same way), `xargs`
  running a deleter, `find -exec rm`, `git rm` (not `--cached`), wrappers
  (`timeout`, `nice`, `env -i`, `stdbuf`, a leading `\`), `dd of=`, `+ref` /
  `:ref` pushes, `checkout -f`, `switch -f` / `--discard-changes`.
- Fewer false refusals: `sudo` / `truncate` / … match only as the program
  (`cargo test truncate`, a commit message, a heredoc body asked);
  `git clean -n` is free; files made by an allowed `tee` / `cp` / `mv` are
  the agent's; moving into a directory checks the file that lands there.
- A refused command's files are no longer recorded as the agent's.
- confirm-dangerous.test.mjs: 70 cases (the probes from both reviews,
  everyday commands, bookkeeping, writes and edits), run in CI with the
  page tests.
- README: the timing column is "tool + after-tool hooks" (the pi adapter
  always has one), with the concurrent-calls caveat; `sed -i` listed among
  what the gate does not read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
@yuanhao
yuanhao merged commit 9d74354 into main Oct 11, 2026
17 checks passed
@yuanhao
yuanhao deleted the fix/handoff-review branch October 11, 2026 13:56
yuanhao added a commit that referenced this pull request Oct 11, 2026
…edocs as the shell does (#298)

From review of #297:
- [P1] `(cd scratch); rm -f owner.txt` passed: the subshell's cd was taken
  to apply to the rm. The tokenizer now marks `( … )`, and the checker
  restores the directory after one. A cd is followed only where it surely
  applies: a literal existing directory, not in a pipeline, the background
  or before `||`; otherwise later paths are unknown and ask.
- [P1] `echo '<<EOF'` + a newline + `rm -f owner.txt` passed: a pre-pass
  regex took quoted text for a heredoc and skipped the rm. Heredocs are now
  recognized in the tokenizer (unquoted `<<WORD`, `<<-`, quoted
  delimiters), their bodies skipped from the next line to the delimiter.
- Tests for both and their variants; on the previous gate they fail on
  exactly the two reported bypasses.
- [P2] README: model and tools do not overlap (the loop awaits each); one
  response's tool calls run concurrently, so their durations do not add.


Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

1 participant