diff --git a/README.md b/README.md index de76166..2462e36 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,46 @@ # dirigent_fermata -`𝄐 fermata` — a fast, harness-agnostic guard that blocks AI coding agents from reading, writing, or executing things they shouldn't. +**A fast, harness-agnostic policy gate for AI coding agents.** -Reads `.botignore` (gitignore syntax) and an optional `botignore.toml` for advanced rules. Designed to be called from agent hooks, used as an MCP server (future), or consumed as a library. +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. -## Status +``` +.env +.env.* +secrets/** +conf/localsettings.yaml +``` -v0.1 — first releasable slice: -- Library: `Op`, `Decision`, `Policy::check`, `Policy::check_command`, project-root walk-up, `.botignore` walker (via `ignore`), `botignore.toml` parsing, path identification heuristics. -- CLI: `fermata check ...`, `fermata hook --harness `. -- Harness: Claude Code (PreToolUse) only. +That's all it takes. -Out of scope for v0.1: Codex, Gemini, MCP server, audit log, filesystem watcher. +--- + +## 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 @@ -23,41 +52,163 @@ cargo install --path crates/dirigent_fermata --features cli This installs the `fermata` binary into `~/.cargo/bin/`. -## Quick start +--- + +## Usage + +### Checking a path ```bash -# As a CLI fermata check --op read /path/to/.env -echo $? # 1 if blocked, 0 if allowed +# exit 1 — blocked +# stderr: blocked by rule ".env" in /your/project/.botignore -# As a Claude Code hook +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` (gitignore syntax, applies to read + write): -``` +### `.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 ``` -`botignore.toml` (per-op rules): +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] -patterns = [".env*", "secrets/**"] +# Block reading secrets outright +patterns = [".env*", "secrets/**", "conf/localsettings.yaml"] [write] +# Allow reading vendor code but block patching it patterns = ["vendor/**", "*.lock"] [bash] -deny = ["rm -rf /", "git push --force*"] +# 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*" +``` + +--- + +## Architecture + +Three concentric layers; nothing inner imports from anything outer: + +- **`core/`** — harness-unaware, sync. Types, `.botignore` walker, `botignore.toml` parser, `Policy::check` / `check_command`, path extraction. +- **`harness/`** — `HarnessAdapter` trait over a normalized `ToolCall`. Each adapter lives in its own submodule, feature-gated. +- **`bin/fermata.rs`** — the only place `clap`, stdio, and exit codes appear. + +--- + ## See also - `docs/tools/fermata.md` — Dirigent integration plan -- `docs/workpad/brainstorm/fermata.md` — full product spec +- `docs/workpad/brainstorm/fermata.md` — full product spec and field notes +- `docs/architecture/crates.md` — crate dependency map