5.5 KiB
𝄐 dirigent_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/settings.local.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 —
.botignoreuses gitignore rules via theignorecrate (the same engine powering ripgrep). - Per-operation control —
botignore.tomllets you block writes tovendor/**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 <path>... |
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
From source (this monorepo):
cargo install --path crates/dirigent_fermata --features cli
This installs the fermata binary into ~/.cargo/bin/.
Usage
Checking a path
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
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.
# Secrets
.env
.env.*
secrets/**
# Local config overrides
conf/settings.local.yaml
conf/settings.test.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:
[read]
# Block reading secrets outright
patterns = [".env*", "secrets/**", "conf/settings.local.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:
{
"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/settings.local.yaml, and a vendor/ tree it doesn't want patched. With .botignore:
.env
.env.*
conf/settings.local.yaml
vendor/**
Claude attempts to read credentials:
Tool: Read
Path: ./conf/settings.local.yaml
Decision: BLOCK — matched rule "conf/settings.local.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:
# botignore.toml
[bash]
deny = ["cat .env*", "cat conf/settings.local*"]
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,.botignorewalker,botignore.tomlparser,Policy::check/check_command, path extraction.harness/—HarnessAdaptertrait over a normalizedToolCall. Each adapter lives in its own submodule, feature-gated.bin/fermata.rs— the only placeclap, stdio, and exit codes appear.
See also
docs/tools/fermata.md— Dirigent integration plandocs/workpad/brainstorm/fermata.md— full product spec and field notesdocs/architecture/crates.md— crate dependency map