Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Contributing

Adding a rule to an existing gate is usually a single TOML edit. Adding a new gate also needs a small Rust file. The build pipeline picks up TOML changes automatically; the generator in build.rs emits the Rust gate function from the TOML on every build.

Adding a tool to an existing gate

Edit the TOML. Done.

Simplest case: add shellcheck to devtools.toml as always-safe.

# rules/devtools.toml
[[programs]]
name           = "shellcheck"
unknown_action = "allow"  # always safe (read-only)

Then cargo build --release. Done. Flag-conditional behavior:

# rules/devtools.toml
[[programs]]
name           = "prettier"
unknown_action = "allow"
[[programs.ask]]
reason        = "Writing formatted files"
if_flags_any  = ["--write", "-w"]

TOML schema

Root-level fields.

FieldDescription
metaRule metadata (priority, lede, behavior tags, custom card titles).
[[programs]]An array of program-specific rules.
safe_commandsAn array of always-allowed command names (read-only command list).
[[conditional_allow]]Conditional rules that allow or gate a program based on flag presence.
[[custom_handlers]]Declares Rust function overrides for complex CLI logic.
[[command_groups]]Docs-only command groupings for Basics gate grid categorization.

TOML schema

Program-level fields (under [[programs]]).

FieldDescription
nameProgram binary name as it appears on PATH.
aliasesAlternative names. Example: ["podman"] aliased to docker.
unknown_actionWhat to do for unrecognised subcommands. One of allow, ask, skip, block.
default_allowBoolean indicating if the command is allowed by default.
reasonDocs-only reason for unknown_action = "allow" programs.
[[programs.allow]]Rules that allow execution. Conditions are optional.
[[programs.ask]]Rules that ask the user for approval. reason is required.
[[programs.block]]Rules that block execution. reason is required.
[[programs.allow_if_flags]]List of flags that automatically allow execution.
[programs.api_rules]Custom HTTP API endpoint routing rules (e.g. safe methods, endpoint prefixes).

Rule conditions

How rules match (under allow/ask/block rules).

FieldDescription
subcommandMatch a specific subcommand string. Example: "pr list".
subcommandsMatch any of the listed subcommands.
subcommand_prefixMatch a subcommand prefix. Example: "describe" matches describe-instances, describe-volumes, etc.
action_prefixMatch a prefix on args[1]. Used for AWS-style describe-* / list-* / create-* patterns.
if_flags_anyApply the rule only if any listed flag is present.
unless_flagsApply the rule unless any listed flag is present.
if_args_containApply the rule if args contain any listed string. Used for path-aware blocks.
unless_args_containApply the rule unless args contain any listed string.
warnOn [[programs.ask]]: mark as dangerous-but-recoverable. Surfaces on the Security floor page.
accept_edits_auto_allowOn [[programs.ask]]: auto-allow this program when the session is in acceptEdits mode.

Custom handlers

When TOML can't express it.

Some logic doesn't fit declarative TOML: path normalisation (rm against / or ~/), method-aware HTTP routing (gh api), shell-script inner-command checking (bash -c). For those, write a Rust handler.

# rules/devtools.toml
[[custom_handlers]]
program     = "ruff"
handler     = "check_ruff"
description = "ruff format asks unless --check or --diff present"

Then implement check_ruff in the gate file. The generated gate returns Skip for this program, letting the custom handler take over.

Custom handlers return one of the same four gate results used by the generated functions:

GateResult::skip()               // this gate has no opinion
GateResult::allow()              // explicitly safe
GateResult::ask("Description")  // mutation or unresolved risk
GateResult::block("Explanation") // deterministic safety floor

Adding a new gate

Three steps.

For a new category of tools that doesn't fit an existing gate.

1. Create rules/newgate.toml. Pick a priority that slots in correctly; lower runs first, basics at 100 is always last.

[meta]
name        = "newgate"
description = "New category of tools"
priority    = 50
[[programs]]
name           = "newtool"
unknown_action = "ask"
[[programs.allow]]
subcommand = "list"
[[programs.ask]]
subcommand = "create"
reason     = "Creating resource"

2. Create src/gates/newgate.rs. Thin wrapper around the generated function.

use crate::generated::rules::check_newgate_gate;
use crate::models::{CommandInfo, GateResult};
pub fn check_newgate(cmd: &CommandInfo) -> GateResult {
    check_newgate_gate(cmd)
}

3. Register in src/gates/mod.rs. Add to the GATES array in priority order.

mod newgate;
pub use newgate::check_newgate;
pub static GATES: &[(&str, GateCheckFn)] = &[
    // … other gates …
    ("newgate", check_newgate),
    ("basics", check_basics), // basics last
];

Simulator

WebAssembly compilation.

The Try page runs a local simulator powered by a WebAssembly build of the gate engine. The compilation uses the release-wasm profile, the wasm32-unknown-unknown target, and compiles only the library target with the wasm feature enabled.

1. Build via mise. The project uses a mise task to build the WebAssembly module. It compiles the Rust codebase and uses Zig CC via a wrapper script to compile the C-based tree-sitter parser without requiring a manual clang/WASI-sdk installation. Run the following command:

$ mise run build-wasm

This compiles the target, runs wasm-bindgen to generate bindings in docs/src/wasm/, and runs wasm-opt with the bulk memory feature enabled to optimize the output size.

Testing

Test rules.

RuleWhy
No real-world data in testsGeneric placeholders only (mytool, $HOME/scripts/deploy/, my-service). Tests are committed to a public repo; never leak usage patterns or personal workflows.
CI portabilityDon't assume specific modern CLI tools (rg, bat, fd) are installed. CI runners have minimal environments. Detect tool availability at runtime and skip gracefully.
Serde output verificationAny struct serialized to JSON for Claude Code needs a test asserting exact field casing. The CLI expects camelCase. Use serde_json::to_string and assert key names.

Read the Reason style guide before writing new reason strings. The 250-char limit, no-em-dashes rule, and authorization-hedge ban are enforced by build.rs and by code review.

tool-gates · permission gate hook for Claude Code, Codex CLI, and Antigravity CLI, with deprecated Gemini CLI support. github.com/camjac251/tool-gates

13 gates · 400+ commands · built from rules/*.toml