Skip to content

feat(hooks): lift the nix run/shell and git push confirmation gates - #2733

Merged
cameronraysmith merged 1 commit into
mainfrom
fm/vx-lift-agent-hook-gates
Aug 17, 2026
Merged

feat(hooks): lift the nix run/shell and git push confirmation gates#2733
cameronraysmith merged 1 commit into
mainfrom
fm/vx-lift-agent-hook-gates

Conversation

@cameronraysmith

Copy link
Copy Markdown
Owner

Lifts the two Claude Code hook gates the captain authorized on 2026-08-17: nix run and git push. Both returned permissionDecision: "ask", which is a hard stall for an agent worker launched with permissions bypassed, and neither command is avoidable in normal work here.

Where the gates actually live

Not in settings.json. Both are escalation arms inside modules/home/tools/hooks/gate-dangerous-commands.sh, a writeShellApplication on PATH that modules/home/ai/claude-code/hooks.nix names as a PreToolUse:Bash hook. The settings file only references the hook by name, so its hook wiring is unchanged by this PR.

What changed

git push auto-permits in every form with an ntfy NOTICE, following the relaxation convention this file already established for non-default-ref pushes (2a2e598), canonical gh pr create -d (d602b2c), and .zt ssh/scp/rsync (d8e2532). That subsumes push_is_safe, so the helper is removed; the cases it used to escalate (force push, delete refspec, push to the default ref) now notify instead of asking.

nix run/nix shell is removed outright rather than converted to a NOTICE. It is a single arm carrying one message, "nix run/shell executes arbitrary code", so lifting the gate the captain named lifts both subcommands. A notification per nix run would be pure noise at the rate this repo invokes it.

Gates inventoried and deliberately left in place

Not touched, for the captain to decide separately:

Gate Protects against
dd, truncate, shred raw writes and secure deletion — the dd gate that was observed firing
sudo privilege escalation
jj git push push to a default bookmark or --all-bookmarks/force
git reset --hard, clean, checkout ., restore ., branch -D, stash drop/clear discarding commits or uncommitted work
mutating gh api/pr/issue/repo/release/workflow/gist outward-facing GitHub mutation
tofu/terraform apply/destroy, kubectl apply/create/delete/exec, helm install/upgrade/uninstall infrastructure and cluster mutation
non-.zt ssh/scp/rsync remote access off the ZeroTier VPN
docker/podman push publishing an image to a registry
pkill/killall, kill with a non-literal PID, xargs kill terminating processes selected by pattern, including sibling agents
find -delete, find -exec rm, xargs rm rm bypass vectors

Companion hooks are untouched: redirect-rm-to-rip (denies rm), gate-mutating-http (asks on mutating curl/wget), gate-git-worktree and gate-worktree-surfaces (ask before making a plain-git worktree), enforce-branch-before-edit (denies edits on the default branch), verify-diamond-before-edit (asks on jj diamond-integrity violations).

Obligations checked

  • Flake check binding the gates — none. modules/checks/hooks.nix is the only oracle driving this script, and its case table covered only the kill arm, so nothing asserted the removed shape. Twenty cases added: eight pinning the lifted behavior, twelve pinning neighbouring gates that must not move.
  • OpenSpec spec mandating the gates — none. The nearest requirement, openspec/specs/pi-agent-environment/spec.md:117, governs Pi's separate TypeScript policy engine (modules/home/ai/pi/policy/permission-rules.ts), which gates neither command. No spec delta needed.
  • Documentation — three surfaces updated: the hook's own --help category list, the commented-out static ask list in claude-code/default.nix that is explicitly retained as documentation of gated categories, and the two places nix-flake-pr-cycle/SKILL.md describes push gating.
  • Generated-settings retraction trap — does not apply, twice over. The gates are script content rather than settings keys, and Claude Code settings are installed wholesale (install -Dm644, claude-code/default.nix:414-430) rather than through modules/home/ai/atomic/merge-settings.sh, which is the mechanism that genuinely cannot retract a key and which only ever touches ~/.atomic/agent/settings.json.

Blast radius

Confined to Claude Code. gate-dangerous-commands is referenced only by modules/home/ai/claude-code/hooks.nix; no other agent consumes it. The firstmate repository's own hooks were not read or modified.

Verification

Ran nix build .#checks.aarch64-darwin.hook-gate-dangerous-commands — the one oracle covering this script — which passes with all 43 cases.

Severity was checked rather than assumed, by replaying the new cases against the pre-change script: six of the eight lifted-gate cases fail there (nix run, nix shell, bare git push, git -C /p push, git push --force origin main), while git push -u origin fm/... and nix build were already permitted and serve as anchors rather than regression detectors. The retained-gate cases hold on both sides.

Behavior was then confirmed against the built derivation, not the source and not this session, since a running session cannot pick up the change without just activate:

allow  | nix run nixpkgs#hello          ask | jj git push
allow  | nix shell nixpkgs#jq           ask | git reset --hard HEAD~1
allow  | git push                       ask | sudo systemctl restart foo
allow  | git push --force origin main   ask | dd if=/dev/zero of=/dev/disk2
allow  | git -C /p push                 ask | pkill watcher

Also evaluated homeConfigurations."crs58@aarch64-darwin".activationPackage to confirm the module edits still evaluate, and built the hook derivation directly, which runs shellcheck via writeShellApplication and so confirms removing push_is_safe left no dead function.

The full just check-fast suite was deliberately not run. Nothing outside this script and its own check changed behavior, and a whole-fleet check run is far wider than the diff.

Incidental finding — not fixed

The gates pattern-match the whole Bash command string, so a gated word in a quoted string or heredoc trips them even though no such command runs. The first commit's own message contains find -delete in prose describing the gates left in place; replaying that commit command against the deployed hook returns ask with reason find -delete removes files. A heredoc body does not escape this because it is still part of the command — passing the message through a file with git commit -F <file> does, which is how the second commit was made.

The three rm-bypass arms are the worst affected because they grep the raw command with no start-of-line or shell-operator anchoring at all, unlike the cmd_match helper the other arms use. Narrowing them is a change to gates this task was not authorized to touch, so they are reported rather than altered.

@cameronraysmith cameronraysmith self-assigned this Aug 17, 2026
Both arms of gate-dangerous-commands returned permissionDecision "ask",
which is a hard stall for an agent worker launched with permissions
bypassed: the confirmation never reaches a human until somebody reads the
pane. Four such stalls were observed across two workers on 2026-08-16 and
2026-08-17, and neither `nix run` nor `git push` is avoidable in normal
work in this repository.

git push now auto-permits in every form with an ntfy NOTICE, following the
relaxation convention this file already established for non-default-ref
pushes (2a2e598), canonical `gh pr create -d` (d602b2c), and .zt
ssh/scp/rsync (d8e2532). This subsumes push_is_safe, so that helper is
removed; the surviving escalating cases it distinguished -- force push,
delete refspec, push to the default ref -- now notify instead of asking.

nix run/shell is removed outright rather than converted to a NOTICE. It is
a single arm carrying one message, "nix run/shell executes arbitrary
code", so lifting the named gate lifts both subcommands; a notification
per `nix run` would be pure noise at the rate this repository invokes it.

Deliberately left in place, as a separate risk class for a separate
decision: raw writes and secure deletion (dd, truncate, shred), sudo,
`jj git push`, git reset --hard / clean / checkout . / restore . /
branch -D / stash drop, mutating gh api / pr / issue / repo / release /
workflow / gist, tofu and terraform apply/destroy, kubectl and helm
mutation, non-.zt ssh/scp/rsync, docker and podman push, pattern-matched
process termination, and the rm bypass vectors (find -delete, find -exec
rm, xargs rm). The companion hooks redirect-rm-to-rip, gate-mutating-http,
gate-git-worktree, gate-worktree-surfaces, enforce-branch-before-edit and
verify-diamond-before-edit are untouched.

Blast radius is confined to Claude Code: gate-dangerous-commands is
referenced only by modules/home/ai/claude-code/hooks.nix. Pi's shell
policy is a separate engine (modules/home/ai/pi/policy/permission-rules.ts)
and gates neither command. No OpenSpec requirement binds these gates, so
no spec delta is needed. The settings.json retraction trap does not apply
on this path twice over: the gates are script content rather than settings
keys, and Claude Code settings are installed wholesale by
claude-code/default.nix rather than through atomic's merge-settings.sh,
which is the mechanism that genuinely cannot retract a key.

Documentation updated in the same change: the hook's own --help category
list, the commented-out static ask list in claude-code/default.nix that is
explicitly retained as documentation of gated categories, and the two
places nix-flake-pr-cycle/SKILL.md describes push gating.

Testing: `nix build .#checks.aarch64-darwin.hook-gate-dangerous-commands`,
the one oracle that drives this script. Its case table previously covered
only the kill arm, so it bound neither removed gate; twenty cases were
added -- eight pinning the lifted behavior, twelve pinning neighbouring
gates that must not move. Severity was checked by replaying the new cases
against the pre-change script: six of the eight lifted-gate cases fail
there, and the retained-gate cases hold on both sides. Also evaluated
homeConfigurations."crs58@aarch64-darwin".activationPackage to confirm the
module edits still evaluate, and built the hook derivation directly, which
runs shellcheck via writeShellApplication and confirms removing
push_is_safe left no dead function. The full `just check-fast` suite was
deliberately not run: nothing outside this script and its own check
changed behavior.
@cameronraysmith
cameronraysmith force-pushed the fm/vx-lift-agent-hook-gates branch from a35b828 to 5aadf36 Compare August 17, 2026 04:52
@cameronraysmith
cameronraysmith marked this pull request as ready for review August 17, 2026 04:57
@cameronraysmith
cameronraysmith merged commit 5aadf36 into main Aug 17, 2026
6 checks passed
@cameronraysmith
cameronraysmith deleted the fm/vx-lift-agent-hook-gates branch August 17, 2026 04:57
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