# 𝄐 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** — `.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

From source (this monorepo):

```bash
cargo install --path crates/dirigent_fermata --features cli
```

This installs the `fermata` binary into `~/.cargo/bin/`.

---

## 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/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:

```toml
[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`:

```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`:

```gitignore
.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:

```toml
# 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, `.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 and field notes
- `docs/architecture/crates.md` — crate dependency map
