What happens between a tool call landing on stdin and a decision returning on stdout. The pipeline runs in the same Rust binary for every client; only the wire-format serialiser at the end varies.
tool-gates internal pipeline
●
Tool-call JSON on stdin
Client auto-detected from hook_event_name (or the --client codex / --client antigravity flag; Antigravity payloads are normalized into the canonical shape first). Routes by tool_name: Bash/Monitor to the gate engine, Write/Edit to file guards + security reminders, MCP to block rules + accept-edits, Skill to auto-approval rules.
▸raw_floor.rs
Raw-string scan
Shared pre-AST checks cover the top-level command plus executable strings inside local shell wrappers and xargs ... shell -c. The hard/soft-ask catalog lives in security_floor.rs; output-cap hard denials live in pipe_caps.rs and run first. A cap denial records a stable cause plus semantic recovery actions, including a source-file selection only when the parser confirms one unambiguous file reader.
▸parser.rs
tree-sitter parse
Bash AST via tree-sitter-bash. Extracts Vec<CommandInfo> with program, args, raw form. Compound commands (&&, ||, |, ;) split into per-segment evaluations. Mise / package.json task expansion runs after parse.
▸gates/*.rs
Gate dispatch
13 gates, ordered by priority. Lower runs first; basics at 100 is always last. Each gate either returns a decision or Skip. Custom handlers in Rust cover what TOML can't express. Strictest decision wins for compound commands.
▸settings.rs
Settings.json merge
Reads four files in priority order (managed → local → project → user). Deny rules win unconditionally. Otherwise: ask vs allow resolved by pattern specificity (longest non-wildcard prefix; ties go to ask). $HOME expansion applied before match.
▷
Decision on stdout
Serialised per client. Claude: nested hookSpecificOutput.permissionDecision with recovery in additionalContext. Codex: empty stdout for non-denies; blocks use nested permissionDecision: "deny" with recovery in additionalContext. Antigravity: flat tightening decisions with deny recovery folded into reason, empty stdout for allow and no-opinion. Gemini (deprecated): flat decision + reason; block recovery is folded into the agent-facing reason, while nonblocking context uses the user-facing systemMessage. At this boundary, recovery.rs maps semantic file recovery to Read, read_file, view_file, or a conditional shell fallback according to FILE_TOOL_SPECS.
Task expansion
Wrapper tasks are evaluated by their contents.
A task runner's name is not enough to classify its effects. Tool Gates resolves supported task definitions, checks the commands they invoke, and uses the strictest resulting decision. Recovery actions survive package-script and mise composition, so a wrapper cannot discard the corrective path attached by an inner denial.
mise tasks
mise run <task>, mise r <task>, and direct mise <task> forms expand from mise.toml or .mise.toml. Dependencies run first, circular dependencies are ignored after the first visit, and a task's dir applies to its command. Built-in mise subcommands are never mistaken for task names.
The mise-generated eval "set -- ${usage_args-}" argument-forwarding prefix, including supported splat and array forms, is stripped before the security floor checks the actual task body.
package scripts
npm run, pnpm run, yarn run, and bun run resolve the named script from the nearest package.json. Non-built-in pnpm, yarn, and bun shorthand forms resolve the same way.
bun <file> and bun run <file> stay file execution when the argument contains a path separator or a recognized code-file extension; they are not looked up as script names.
Why the order matters
Each stage closes a gap the next can't.
Raw-string before AST
Pipe-to-shell and eval are caught before parsing because tree-sitter sees them as syntactically valid bash. The AST doesn't know that curl | bash is the security threat; the raw-string pass does.
Custom handlers before declarative
Path-aware rm normalisation, gh-api method routing, sudo command extraction. These need imperative Rust because they parse the inner structure of the command, not just match on subcommand strings.
Gates priority-ordered
Specific gates (git, gh, cloud) decide before basics catches anything as safe. Otherwise git status would be allowed by basics before the git gate could surface its real reason text.
Settings merge last
Gate blocks always win over settings.json (the safety floor is not configurable). Otherwise: explicit deny > explicit ask > explicit allow > gate decision > unknown (defer or ask).
Side data
Recovery, hints, tracking, security scan.
Four subsystems run alongside the main pipeline without changing the decision.
Subsystem
Source
What it does
Recovery guidance
recovery.rs
Stores corrective steps as semantic actions, then renders them with the active client's registered capabilities. Causes remain stable across clients; native tool names appear only at serialization. Surfaces without an active hook client, such as the WASM simulator, render neutral recovery without naming a tool.
Modern CLI hints
hints.rs
For allowed commands using legacy tools, attach a client-neutral suggestion through the context channel that client supports: Claude agent context, Gemini's user-facing systemMessage, Codex PostToolUse context, and no output for Antigravity no-opinion decisions. Gated on the modern tool being installed (7-day cache in tool_cache.rs). Session-deduped via hint_tracker.rs.
Approval tracking
tracking.rs
PreToolUse → PostToolUse correlation with 24h TTL. Successful asks land in ~/.cache/tool-gates/pending.jsonl for the review TUI.
Security reminders
security_reminders.rs
Three-tier scan of Write/Edit bodies. Tier 1 denies source writes before they land, with doc-file secrets nudged after write; Tier 2 nudges via PostToolUse; Tier 3 warns on additionalContext. See the Security reminders page.
Generated gate code lives under src/generated/rules.rs. Built by build.rs from every rules/*.toml on every cargo build. Do not edit by hand; changes are overwritten. The generator also emits per-gate check_*_gate() functions that the Rust gate files wrap and extend with custom handlers.