# 𝄐 fermata **A fast, harness-agnostic policy gate for AI coding agents.** Drop a `.botignore` file in your project root. Fermata reads it and blocks your agent from reading, writing, or running things it shouldn't — before the tool call happens. ``` .env .env.* secrets/** conf/localsettings.yaml ``` That's all it takes. --- ## Why Fermata AI coding agents are powerful, but they don't have an innate sense of "don't touch `.env`." Native hook systems in tools like Claude Code let you intercept every file operation — but wiring up your own secure, fast hook for each project is friction. Fermata is that hook, ready to drop in. - **Fast** — written in Rust; ~1–5ms per call. Hooks fire on every read, write, and bash operation. Python cold-start (~50–150ms) compounds fast. Fermata doesn't. - **Familiar syntax** — `.botignore` uses gitignore rules via the `ignore` crate (the same engine powering ripgrep). - **Per-operation control** — `botignore.toml` lets you block writes to `vendor/**` while still allowing reads, or deny specific bash patterns without touching path rules. - **Harness-agnostic** — plain CLI exit codes work from any shell wrapper; the hook adapter speaks Claude Code's JSON natively. --- ## Status: v0.1 | Component | Status | |-----------|--------| | Library (`Op`, `Decision`, `Policy::check`, `Policy::check_command`) | Done | | `.botignore` walker (project-root walk-up, gitignore semantics) | Done | | `botignore.toml` parser (read / write / bash namespaces) | Done | | Path identification heuristics | Done | | CLI: `fermata check ...` | Done | | CLI: `fermata hook --harness claude` | Done | | Claude Code PreToolUse adapter | Done | Out of scope for v0.1: Codex / Gemini hook adapters, MCP server mode, audit log, filesystem watcher. --- ## Install ```bash cargo install --git https://git.g4b.org/dirigence/fermata --features cli ``` This installs the `fermata` binary into `~/.cargo/bin/`. Requires a working [Rust toolchain](https://rustup.rs). --- ## Usage ### Checking a path ```bash fermata check --op read /path/to/.env # exit 1 — blocked # stderr: blocked by rule ".env" in /your/project/.botignore fermata check --op write /path/to/src/main.rs # exit 0 — allowed ``` ### Claude Code hook adapter ```bash fermata hook --harness claude < hook_payload.json ``` Reads the PreToolUse JSON from stdin, extracts the tool name and path or command, applies policy, and emits the Claude-shaped JSON response. The hook's exit code is always `0`; the verdict is in the JSON body. --- ## Configuration ### `.botignore` — the 80% case Create a `.botignore` at your project root. Gitignore syntax. Blocks both reads and writes. ```gitignore # Secrets .env .env.* secrets/** # Local config overrides conf/localsettings.yaml conf/localtestsettings.yaml # Generated files — let the tools rebuild them, not patch them dist/** *.lock ``` Fermata walks up from the target file to find the nearest `.botignore`, so it works correctly even when an agent changes directory. ### `botignore.toml` — per-operation rules For cases where `.botignore`'s uniform read+write block isn't granular enough: ```toml [read] # Block reading secrets outright patterns = [".env*", "secrets/**", "conf/localsettings.yaml"] [write] # Allow reading vendor code but block patching it patterns = ["vendor/**", "*.lock"] [bash] # Hard-block destructive or exfiltrating commands deny = [ "rm -rf /", "curl * | sh", "git push --force*", ] # Ask before any removal or move ask = ["rm:*", "mv:*"] # Narrow allowlist for automated commands allow_prefixes = ["make test", "git checkout:*"] ``` --- ## How it fits into Claude Code Add fermata as a `PreToolUse` hook in `.claude/settings.json`: ```json { "hooks": { "PreToolUse": [ { "matcher": "Bash|Read|Edit|Write", "hooks": [ { "type": "command", "command": "fermata hook --harness claude" } ] } ] } } ``` When Claude attempts a `Read(.env)`, `Write(vendor/foo.js)`, or `Bash(rm ./secrets/key.pem)`, fermata intercepts the call, checks policy, and returns a deny with a human-readable reason — before any damage is done. --- ## Real-world scenario A project has `.env`, `conf/localsettings.yaml`, and a `vendor/` tree it doesn't want patched. With `.botignore`: ```gitignore .env .env.* conf/localsettings.yaml vendor/** ``` Claude attempts to read credentials: ``` Tool: Read Path: ./conf/localsettings.yaml Decision: BLOCK — matched rule "conf/localsettings.yaml" (.botignore) ``` Claude attempts to read application code: ``` Tool: Read Path: ./src/app/main.rs Decision: ALLOW ``` Claude attempts to run `cat .env` via bash — which would bypass a path-only check: ```toml # botignore.toml [bash] deny = ["cat .env*", "cat conf/localsettings*"] ``` ``` Tool: Bash Command: cat .env Decision: BLOCK — matched bash deny rule "cat .env*" ``` --- ## Harness support | Harness | v0.1 | |---------|------| | Claude Code (PreToolUse) | Supported — native JSON adapter | | Codex CLI | Planned | | Gemini CLI | Planned (via MCP server mode) | | Any shell-based hook | Supported — CLI exit codes | --- ## About this repository This is a downstream mirror. Fermata is developed inside the upstream [Dirigent](https://git.g4b.org/dirigence/dirigent) monorepo and exported here for standalone distribution. Issues and pull requests are accepted on the `develop` branch, but the canonical development happens upstream. --- ## License Licensed under either of - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE)) - MIT License ([LICENSE-MIT](LICENSE-MIT)) at your option.