docs(fermata): redesign READMEs with polished formatting and examples
Rewrote both the monorepo and export READMEs with a punchy tagline, status table, realistic before/after scenarios (including the bash bypass case), full configuration reference, and Claude Code hook integration guide. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,46 @@
|
|||||||
# dirigent_fermata
|
# 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:
|
That's all it takes.
|
||||||
- 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 <path>...`, `fermata hook --harness <name>`.
|
|
||||||
- Harness: Claude Code (PreToolUse) only.
|
|
||||||
|
|
||||||
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 <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
|
## Install
|
||||||
|
|
||||||
@@ -23,41 +52,163 @@ cargo install --path crates/dirigent_fermata --features cli
|
|||||||
|
|
||||||
This installs the `fermata` binary into `~/.cargo/bin/`.
|
This installs the `fermata` binary into `~/.cargo/bin/`.
|
||||||
|
|
||||||
## Quick start
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Checking a path
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# As a CLI
|
|
||||||
fermata check --op read /path/to/.env
|
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
|
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
|
## 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
|
||||||
.env.*
|
.env.*
|
||||||
secrets/**
|
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
|
```toml
|
||||||
[read]
|
[read]
|
||||||
patterns = [".env*", "secrets/**"]
|
# Block reading secrets outright
|
||||||
|
patterns = [".env*", "secrets/**", "conf/localsettings.yaml"]
|
||||||
|
|
||||||
[write]
|
[write]
|
||||||
|
# Allow reading vendor code but block patching it
|
||||||
patterns = ["vendor/**", "*.lock"]
|
patterns = ["vendor/**", "*.lock"]
|
||||||
|
|
||||||
[bash]
|
[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:*"]
|
ask = ["rm:*", "mv:*"]
|
||||||
|
# Narrow allowlist for automated commands
|
||||||
allow_prefixes = ["make test", "git checkout:*"]
|
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
|
## See also
|
||||||
|
|
||||||
- `docs/tools/fermata.md` — Dirigent integration plan
|
- `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
|
||||||
|
|||||||
Reference in New Issue
Block a user