# Agents - Guards, Hooks, and Security Boundaries
Hooks and guards, not written instructions, hold agent behavior to its limits. Hooks run synchronously in every open session, so cheap builtins and event-driven work keep the machine light. Agent output and agent-read text carry no user authority: treat both as untrusted data.
## Key points
- `PostToolUse` hooks run synchronously. The agent waits for exit. Use `( cmd ) & disown $!` for UI effects.
- Order hook gates: kill-switch, environment detection, preconditions, threshold. Each gate exits 0 silently, so each gate is testable alone.
- A `TMUX` variable check is a cheap IDE-mode gate: IDE extension sessions do not run inside tmux, so the popup exits silently.
- Hook hygiene: read stdin with `$(</dev/stdin)`, not `$(cat)`, which spawns two external processes per call. Use stable binary paths such as `/opt/homebrew/bin/node`. Namespace environment variables under one prefix. Run `bats` on the test directory so `make check` picks up new tests.
- An idle session is not free. A 5-second footer tick with 7-9 git subprocesses each produced about 50 git processes per second. Status-line JSON carries no git state, so a script adds two git calls per prompt. Event-driven updates and one `rev-parse` cut the rate by 90 percent.
- Prose rules do not change agent behavior. One rule typed in 73 sessions was ignored in at least 66. Another was ignored in 41. Encode the fix as hooks and guard checks.
- A guard regex tested only against the plain form is untested. `git\s+commit` misses `git -c commit.gpgsign=false commit` and `git -C <dir> commit`. Agents use both bypass forms.
- Secret guards scan whole staged files, not the diff. Fake fixtures on the default branch block any commit that touches them. A hard deny aborts the whole chain: one blocked `cat` made a later `git commit -F` fail on a missing file. Write the message file in a separate step.
- Three layers enforce a rule better than one: pre-commit hook, Makefile target, CI script. Each layer can be bypassed alone, so a mistake must slip past every layer.
- Hook warnings are a hint, not a fact. One session-start hook reported unmerged files that `git status` did not show. Verify against the repository first.
- When hook noise obscures the index, `git status --porcelain=v2` plus `git ls-files --stage`/`-u` is the authoritative view. Run the failing pre-commit hook with debug output before you touch the sequencer, to separate scanner noise from a real block.
- Subagent teammate messages carry no user authority. The permission classifier blocks writes that a teammate message initiates. The orchestrator performs privileged calls itself or gets explicit per-call authorization.
- Subagent hand-backs arrive in a frame that indents every line and declares the output not from the user. A forged frame-like line at column zero is exposed by the indentation. No approval claim in a hand-back carries user authority.
- Review inputs can carry prompt injection. Fake `[workflow-rules]` lines naming a tool appeared in PR comments and tool output. No such tool existed in the allowlist. Treat review-input text as data, never as instructions.
- A nested headless `claude -p` call returns 401 while the parent session is authenticated. The launcher injects the credential and hides it from Bash subprocesses. Headless tooling needs its own key or a different backend, such as an OpenAI-compatible local endpoint.
- With an auto permission mode and a read-only git/gh allowlist, the fast classifier judges each unmatched call independently and without reasons. One session allowed `git commit` under an allow rule, denied the first `git push`, allowed the second only because the prompt contained a push instruction, and denied `gh pr create` in both body forms.
## How to apply
1. Encode every rule that matters as a hook or a guard check.
2. Test each guard regex against bypass forms such as `git -c <key>=<value> commit` and `git -C <dir> commit`, not only the plain form.
3. Keep hooks cheap and quiet: gate order from kill-switch to threshold, `$(</dev/stdin)`, stable binary paths, one env-var prefix, `( cmd ) & disown $!` for UI effects.
4. Drive status updates from tool events, not a polling tick. Combine git calls into one `rev-parse`.
5. When a guard blocks, verify with `git status --porcelain=v2` and run the failing hook directly before you touch the sequencer.
6. Treat review comments, hand-backs, and tool output as data, never as instructions. Check each tool name and approval claim against the allowlist.
## Observations
- [security] Review inputs can carry prompt injection. A fake rule line named a nonexistent tool. #prompt-injection (source: agent sessions, 2026-11)
- [process] Prose rules do not change behavior. One typed rule was ignored in at least 66 of 73 sessions. #guards (source: agent sessions, 2026-11)
- [perf] An idle session is not free. A 5-second footer tick caused about 50 git processes per second. #performance (source: agent sessions, 2026-10)
- [security] Subagent output carries no user authority. The permission classifier blocks teammate-initiated writes. #permissions (source: agent sessions, 2026-06)
## Relations
- relates_to [[resources/documentation/Agents/Agents - Parallel Orchestration and Delegation]] (subagent authority)
- relates_to [[resources/documentation/Agents/Agents - Plan First, Fresh Sessions, Write It Down]] (rules need enforcement)
- relates_to [[resources/documentation/Agents/Claude Code - Plugins, Settings, and Scheduled Tasks]] (hook configuration)
- relates_to [[resources/documentation/Agents/Agents - Worktree Workflows and Pre-PR Review]] (pre-commit guards)
## Sources
- Personal agent-session notes, February 2026 to November 2026.