𝄐 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 —
.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
cargo install --git https://git.g4b.org/dirigence/fermata --features cli
This installs the fermata binary into ~/.cargo/bin/.
Requires a working Rust toolchain.
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/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:
[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:
{
"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:
.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:
# 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 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)
- MIT License (LICENSE-MIT)
at your option.