🛰️ export standalone-repo assets (c86caab7)

This commit is contained in:
2026-05-29 18:19:22 +02:00
parent 168aefd415
commit ebd5abeac4
5 changed files with 288 additions and 219 deletions
-43
View File
@@ -1,43 +0,0 @@
# Package: dirigent_fermata
Harness-agnostic secret redaction engine and policy gate for AI coding agents.
## Quick Facts
- **Type**: Library + binary (`fermata`)
- **Main Entry**: `src/lib.rs`, `src/bin/fermata.rs`
- **Dependencies**: `ignore`, `toml`, `regex`, `globset`, `serde`, `clap` (cli feature), `aho-corasick`, `serde_yaml`
- **Status**: v0.2 — secret redaction + policy gate (`.botsecrets` now includes `[policy]` section)
## Layering
Three concentric layers; nothing inner imports from anything outer.
- **`core/`** — harness-unaware, transport-unaware, sync. Types (`Op`, `Decision`), `.botignore` walker, `Policy::check` / `check_command`, path extraction. `.botsecrets` contains a `[policy]` section for access control rules (`fermata.toml` is accepted as an alias for `.botsecrets`). Sync, no tokio.
- **`core/secrets/`** — the secret filtering engine:
- `config.rs``.botsecrets` TOML parser (`SecretsConfig`, `PolicyConfig`) and hierarchical resolution (user, project, local override).
- `manifest.rs` — discovers secret-containing files from `.botsecrets` patterns and loads their content for redaction.
- `parser.rs` — multi-format secret file parser (`.env`, TOML, YAML, JSON). Extracts key-value pairs where the value is a secret.
- `patterns.rs` — built-in key name patterns (~30 universal patterns like `*_KEY`, `*_SECRET`, `*_PASSWORD`) and gitleaks-derived regex patterns for heuristic scanning.
- `redactor.rs``Redactor` builds an Aho-Corasick automaton from known secret values and replaces them in arbitrary text. Sub-millisecond performance.
- `scanner.rs``Scanner` applies heuristic regex patterns to detect secrets not covered by the known-value manifest (entropy-based and format-based detection).
- **`harness/`** — `HarnessAdapter` trait over a normalized `ToolCall` (PreToolUse) and `PostToolUsePayload` (PostToolUse). Each adapter (Claude, future Codex, etc.) lives in its own submodule, feature-gated. PostToolUse enables output redaction via `updatedToolOutput` before content enters the LLM context.
- **`bin/fermata.rs`** — only place where `clap`, stdio, and exit codes appear.
## Release Model
Developed in this monorepo; planned to be exported as a standalone repo in the future for advertising / external distribution. Development stays here. See `docs/tools/fermata.md`.
## Dependency Direction
`dirigent_tools` depends on `dirigent_fermata`, never the reverse. Fermata must remain usable as a standalone hook/MCP without dragging in the in-process ACP tool runtime.
## Out of scope (v0.2)
Codex / Gemini hook adapters, MCP server mode, `readonly_only` Bash mode, audit log, filesystem watcher, context taint tracking. Each is a future task with its own plan.
## See also
- `docs/tools/fermata.md` — Dirigent integration plan
- `docs/workpad/brainstorm/fermata.md` — canonical product spec
- `docs/architecture/fermata-security-philosophy.md` — security philosophy and the reveal triangle
- `.botsecrets` format: `core/secrets/config.rs` — unified config for secret redaction and policy (`fermata.toml` accepted as alias)
+42 -45
View File
@@ -1,45 +1,42 @@
[package]
name = "dirigent_fermata"
version = "0.1.0"
edition = "2021"
rust-version = "1.75"
description = "Harness-agnostic policy gate for AI coding agents (.botignore + .botsecrets)"
license = "MIT OR Apache-2.0"
repository = "https://git.g4b.org/dirigence/fermata"
readme = "README.md"
keywords = ["ai", "agents", "security", "policy", "gitignore"]
categories = ["command-line-utilities", "development-tools"]
[lib]
path = "src/lib.rs"
[[bin]]
name = "fermata"
path = "src/bin/fermata.rs"
required-features = ["cli"]
[dependencies]
aho-corasick = "1.1"
globset = "0.4"
ignore = "0.4"
walkdir = "2"
toml = "0.8"
regex = "1.10"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.9"
thiserror = "2.0"
clap = { version = "4.5", features = ["derive"], optional = true }
[dev-dependencies]
tempfile = "3.10"
assert_cmd = "2.0"
predicates = "3.1"
[features]
default = ["cli", "harness-claude"]
cli = ["dep:clap"]
harness-claude = []
[lints]
workspace = true
[package]
name = "dirigent_fermata"
version = "0.1.0"
edition = "2021"
rust-version = "1.75"
description = "Harness-agnostic policy gate for AI coding agents (.botignore + .botsecrets)"
license = "MIT OR Apache-2.0"
repository = "https://git.g4b.org/dirigence/fermata"
readme = "README.md"
keywords = ["ai", "agents", "security", "policy", "gitignore"]
categories = ["command-line-utilities", "development-tools"]
[lib]
path = "src/lib.rs"
[[bin]]
name = "fermata"
path = "src/bin/fermata.rs"
required-features = ["cli"]
[dependencies]
aho-corasick = "1.1"
globset = "0.4"
ignore = "0.4"
walkdir = "2"
toml = "0.8"
regex = "1.10"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.9"
thiserror = "2.0"
clap = { version = "4.5", features = ["derive"], optional = true }
[dev-dependencies]
tempfile = "3.10"
assert_cmd = "2.0"
predicates = "3.1"
[features]
default = ["cli", "harness-claude"]
cli = ["dep:clap"]
harness-claude = []
+103 -131
View File
@@ -1,35 +1,60 @@
# fermata
# 𝄐 fermata
**A fast, harness-agnostic security layer for AI coding agents.**
**The security layer for AI coding agents.**
AI coding agents read files, run commands, and inspect output as part of their normal workflow. When they read `.env`, secret values get tokenized into the LLM's context window -- and from there they can leak into commits, PR descriptions, log messages, or API calls. The solution is not blocking the read -- the agent needs to see config structure and key names to reason about your project. The solution is **redacting secret values from the output before they reach the model**. No AI coding agent ships built-in post-read secret filtering today. fermata fixes that.
AI coding agents read files, run shell commands, and inspect output as part of normal work. When they read `.env`, the secret values get tokenized into the LLM's context window. From there, they can leak into commits, PR descriptions, or API calls the agent makes. The secret is irrecoverably revealed.
## Why
fermata sits between the agent and its tools. It blocks operations that shouldn't happen, and scrubs secret values from the output of operations that should.
Blocking reads is the wrong approach. The agent needs to see file structure. It needs to know which keys exist in `.env`, what your database config looks like, how your secrets are organized. What it does *not* need to see is the actual secret values. An agent can have full read access to `.env` without secret values being revealed -- if the output is redacted before it reaches the model.
> [!CAUTION]
> **Alpha software.** Fermata is functional and in daily use by the author, but not widely tested across diverse environments. The core library and Claude Code hook adapters are production-grade; other features are earlier in maturity. Expect rough edges and breaking changes.
fermata operates on two independent levels:
---
- **Secret filtering** (PostToolUse) -- `.botsecrets` declares where secrets live; fermata parses them, builds an Aho-Corasick automaton, and redacts secret *values* from tool output before they enter the LLM context. This is the primary defense. It catches secrets regardless of how they appear -- direct reads, shell output, log files, error messages.
- **Policy gate** (PreToolUse) -- `.botsecrets [policy]` / `.botignore` blocks dangerous writes and destructive commands before they execute. Supplementary protection for write safety and anti-jailbreak.
## The Problem
The key insight: file-level access control operates on file identity (*which file*). Secret redaction operates on data content (*which values*). The reveal problem can only be solved at the data-content level.
Traditional security blocks the file. But secrets also appear in shell output, log files, error messages, environment variable dumps, and indirect reads that bypass any access-control list.
> **Note:** fermata also accepts `fermata.toml` as an alias for `.botsecrets` (same format, `.botsecrets` takes priority when both exist).
<p align="center">
<img src="threat-landscape.svg" alt="Where secrets leak from — blocking the file is necessary but not sufficient" width="720">
</p>
The actual concern is not "can the agent open this file?" but "do secret *values* enter the LLM context?" An agent can have read access to `.env` without the secret values being revealed — if the output is redacted before it reaches the model.
---
## How It Works
fermata interposes on the tool lifecycle at two points:
<p align="center">
<img src="interception-flow.svg" alt="How fermata intercepts — PreToolUse blocks, PostToolUse redacts" width="720">
</p>
**PreToolUse** — Before the tool executes, fermata checks `.botsecrets [policy]` and `.botignore` rules against the operation. A blocked write never happens. A blocked command never runs. Most harnesses already handle basic file blocking, but fermata catches stragglers and works in permissive/yolo modes too.
**PostToolUse** — After the tool executes, fermata scans the output for secret values. Declared secrets (loaded from files matched by `.botsecrets`) are replaced using an Aho-Corasick automaton — zero false negatives, sub-millisecond. A secondary heuristic scan catches undeclared secrets that match known formats (AWS keys, JWTs, GitHub PATs, database URLs). This is the primary defense layer.
This means `source .env && echo $DB_PASSWORD` is caught even though no file read was blocked — the secret value itself is scrubbed from the output before the LLM ever sees it.
---
## Quick Start
### Install
```bash
cargo install --path . --features cli
cargo install --git https://git.g4b.org/dirigence/fermata --features cli
```
### Protect a project in 30 seconds
Requires a working [Rust toolchain](https://rustup.rs).
```bash
# Declare where secrets live -- fermata parses them and redacts values from agent output
cat > .botsecrets << 'EOF'
### Protect a project
Create a `.botsecrets` file at your project root — the primary (and usually only) config you need:
```toml
# .botsecrets
[files]
patterns = [".env", ".env.*", "secrets.*"]
@@ -38,10 +63,21 @@ patterns = [".claude/**", "vendor/**", "*.lock"]
[policy.bash]
deny = ["rm -rf /", "curl * | sh"]
EOF
```
One file. The agent can read `.env` freely -- fermata redacts the secret values from the output before they reach the model. Write protection and bash safety rules live in the same `.botsecrets` under `[policy]`.
One file. The agent can read `.env` freely fermata redacts the secret values from the output before they reach the model. Write protection and bash safety rules live in the same `.botsecrets` under `[policy]`.
fermata ships with built-in key patterns (`*_KEY`, `*_SECRET`, `*_PASSWORD`, `*_TOKEN`, `DATABASE_URL`, and ~25 more) that cover the common cases automatically.
> **Note:** fermata also accepts `fermata.toml` as an alias for `.botsecrets` (same format, `.botsecrets` takes priority when both exist).
Optionally, add a `.botignore` for simple path blocking using gitignore syntax:
```gitignore
# .botignore (optional — complements .botsecrets)
vendor/
*.lock
```
### Wire into Claude Code
@@ -70,153 +106,89 @@ Add both hooks in `.claude/settings.json`:
}
```
That's it. PostToolUse redacts secret values from tool output before they reach the LLM. PreToolUse blocks forbidden writes and dangerous commands.
PostToolUse redacts secret values from output before they reach the LLM. PreToolUse blocks forbidden writes and dangerous commands.
## How It Works
---
fermata interposes on every tool call in the agent's lifecycle:
## What Fermata Does Not Do
```
Agent wants to run a tool
|
PreToolUse ── .botsecrets [policy] / .botignore
| write blocked? → deny
| bash denied? → deny
| otherwise → allow (including reads of .env!)
|
Tool executes
|
PostToolUse ── .botsecrets [files] + [keys] + [heuristic]
| secret values found? → redact before LLM sees it
|
Clean output enters LLM context
```
fermata is a heuristic guard, not a sandbox. It defends against statistical agent behavior — the unguided LLM reaching for `.env`, the overly-broad glob, the stray `cat` of a credential file. It does not defend against a deliberate adversary trying to escape the box.
Three layers of defense, each independent:
Things fermata cannot catch:
| Layer | Mechanism | What it catches |
|-------|-----------|-----------------|
| **Known-value redaction** | `.botsecrets` declares secret files; fermata parses them and builds an Aho-Corasick automaton | Every occurrence of a declared secret value, in any tool output, regardless of source |
| **Heuristic detection** | Regex patterns from gitleaks detect undeclared secrets (AWS keys, JWTs, GitHub PATs, database URLs) | Secrets not covered by the manifest -- runtime-generated, unexpected locations |
| **Access control** | `.botsecrets [policy]` / `.botignore` rules block writes and dangerous commands | Destructive writes, anti-jailbreak (agent modifying its own hooks), dangerous shell commands |
- **Network exfiltration** — an agent sending secrets via `curl` or `git push`. Use network-level controls (firewall, container networking) for this.
- **Kernel-level file access** — a process bypassing tool hooks entirely. Use container isolation or a sandbox for hard filesystem boundaries.
- **Character-by-character reconstruction** — an adversarial agent reassembling a secret across multiple tool calls.
Performance: ~1-5ms per tool call. Cold start (loading config + parsing secret files) is ~10-20ms.
These are honest boundaries, not future promises. See [docs/threat-model.md](docs/threat-model.md) for the full analysis.
---
## Configuration
### `.botsecrets` -- the primary (and usually only) config
**`.botsecrets`** is the primary configuration file. It declares which files contain secrets (`[files]`), how to redact them (`[redaction]`), and optionally embeds access-control policy (`[policy.write]`, `[policy.bash]`, `[policy.read]`). Most projects need only this file. `.botsecrets` can do everything `.botignore` can and more.
`.botsecrets` is the unified configuration file. It declares both what to redact and what to restrict:
**`.botignore`** uses gitignore syntax to block reads and writes. Useful for monorepo subtree exclusion or teams that prefer gitignore syntax for simple path blocking. Complements `.botsecrets` but is not required.
```toml
[files]
patterns = [".env", ".env.*", "secrets.*"]
See [docs/configuration.md](docs/configuration.md) for the full reference with examples.
[keys]
include = ["STRIPE_*", "MY_APP_SIGNING_*"]
---
[heuristic]
enabled = true
## Status
# Access control: write protection and bash safety.
# Reading secret-containing files is allowed -- Layer 1 redacts the values.
v0.2 — secret filtering engine and policy gate are production-ready:
[policy.write]
patterns = [".claude/**", "vendor/**", "*.lock"]
| Component | Status | Maturity |
|-----------|--------|----------|
| `.botsecrets` config + `[policy]` section | Done | production |
| `.botignore` walker (gitignore semantics) | Done | production |
| Known-value redactor (Aho-Corasick) | Done | production |
| Heuristic scanner (gitleaks-derived patterns) | Done | production |
| Multi-format secret parser (.env, TOML, YAML, JSON) | Done | production |
| Claude Code PreToolUse + PostToolUse adapters | Done | production |
| CLI: `fermata check` and `fermata hook` | Done | production |
[policy.bash]
deny = ["rm -rf /", "curl * | sh"]
```
Out of scope for v0.2: Codex / Gemini hook adapters, MCP server mode, audit log, filesystem watcher.
Built-in key patterns (`*_KEY`, `*_SECRET`, `*_PASSWORD`, `*_TOKEN`, `DATABASE_URL`, etc.) handle most projects without custom configuration.
### `.botignore` -- optional simple layer
Gitignore syntax. For projects that want a minimal, familiar format for write protection. Complements `.botsecrets` but is not required.
```gitignore
vendor/**
*.lock
```
See [docs/configuration.md](docs/configuration.md) for the full reference.
## Commands
```bash
# Check if a path is allowed
fermata check --op read /path/to/.env # exit 1 = blocked
fermata check --op write src/main.rs # exit 0 = allowed
# Run as a hook (reads harness JSON from stdin)
fermata hook --harness claude
fermata hook --harness claude --event post-tool-use
```
See [docs/commands.md](docs/commands.md) for the full CLI reference.
## Library API
fermata is also a Rust library:
```rust
use dirigent_fermata::core::secrets::{Manifest, Redactor, Scanner, SecretsConfig};
// Load .botsecrets and build the redaction manifest
let config = SecretsConfig::load("/path/to/project")?;
let manifest = Manifest::discover(&config)?;
// Known-value redaction (Aho-Corasick, sub-millisecond)
let redactor = Redactor::from_manifest(&manifest);
let clean = redactor.redact("DB_PASSWORD=hunter2");
// -> "DB_PASSWORD=*****"
// Heuristic scanning (regex patterns)
let scanner = Scanner::new(&config);
let findings = scanner.scan("Found key: AKIA1234567890ABCDEF");
// -> [Finding { pattern: "AWS Access Key", confidence: High, .. }]
```
## Security Model
fermata addresses a novel security concern: **reveal** -- whether secret *values* enter the LLM context. Traditional file-level access control operates on file identity (which file). Secret redaction operates on data content (which values). The reveal problem can only be solved at the data-content level.
Read [docs/security-model.md](docs/security-model.md) for the full analysis, including the Reveal Triangle and defense-in-depth architecture.
## Threat Model
fermata is a heuristic guard, not a sandbox. It defends against statistical agent behavior and prompt-driven mistakes -- not a deliberate adversary. This is a strength: the threat model is well-defined, and the boundaries are documented honestly.
Read [docs/threat-model.md](docs/threat-model.md) for what fermata catches, what it doesn't, and what to combine it with.
---
## Harness Support
| Harness | Status | Mechanism |
|---------|--------|-----------|
| Claude Code | Shipped | PreToolUse + PostToolUse hooks |
| Codex CLI | Planned | Pre-exec hook adapter |
| Codex CLI | Planned | Hook adapter |
| Gemini CLI | Planned | MCP server mode |
| Any MCP agent | Planned | MCP proxy wrapping existing servers |
| Any shell-based hook | Supported | CLI exit codes |
The policy engine and redaction logic are identical across all modes. Only the I/O adapter changes.
## Status
---
v0.2 -- secret filtering engine and policy gate are production-ready. All core components are implemented and tested:
## Background
- `.botsecrets` config with `[files]`, `[keys]`, `[heuristic]`, and `[policy]` sections
- Aho-Corasick known-value redactor
- Heuristic scanner with gitleaks-derived patterns
- Manifest discovery, multi-format parser (.env, TOML, YAML, JSON)
- Claude Code PreToolUse and PostToolUse adapters
- `.botignore` walker with gitignore semantics
fermata addresses a novel security concern — **reveal**: whether secret *values* enter the LLM context, independent of whether the agent can open a file. This distinction (file identity vs. data content) is explored in:
## The `.botsecrets` Vision
- [docs/security-model.md](docs/security-model.md) — the Reveal Triangle and defense-in-depth architecture
- [docs/threat-model.md](docs/threat-model.md) — what fermata catches at each detection level, and where it stops
- [docs/commands.md](docs/commands.md) — full CLI reference
`.botsecrets` is designed to be the **`.gitignore` of AI agent security**: a simple, declarative, human-readable file that every project can drop in to protect its secrets from AI agents.
The `.botsecrets` format is designed to be the **`.gitignore` of AI agent security**: a simple, declarative, harness-agnostic file that every project can drop in. The portable sections (`[files]`, `[keys]`, `[redaction]`, `[heuristic]`) declare *what* to protect; the `[policy]` section adds fermata-specific access control.
The format is harness-agnostic from day one. It declares *what* to protect, not *how*. One file covers both redaction (`[files]`, `[keys]`, `[heuristic]`) and access control (`[policy]`). The same `.botsecrets` works with Claude Code, Codex, Gemini, and any future harness that supports tool lifecycle hooks.
---
## Part of Dirigent
Fermata is the security subsystem of [Dirigent](https://git.g4b.org/dirigence/dirigent), a multi-agent orchestration platform. It is developed in the upstream monorepo and exported here for standalone use — no other Dirigent component is required.
---
## License
Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or [MIT License](LICENSE-MIT) at your option.
Licensed under either of
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE))
- MIT License ([LICENSE-MIT](LICENSE-MIT))
at your option.
+72
View File
@@ -0,0 +1,72 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 480" width="720" height="480" font-family="system-ui, -apple-system, sans-serif" font-size="11">
<rect x="0" y="0" width="720" height="480" rx="8" fill="#f8f9fa"/>
<text x="360" y="24" text-anchor="middle" font-size="13" font-weight="bold" fill="#1a1a2e">How fermata intercepts</text>
<!-- Agent request -->
<rect x="60" y="42" width="280" height="32" rx="6" fill="#dfe6e9" stroke="#636e72" stroke-width="1"/>
<text x="200" y="62" text-anchor="middle" fill="#2d3436" font-size="11" font-weight="600">Agent requests a tool call</text>
<!-- Arrow down -->
<line x1="200" y1="74" x2="200" y2="92" stroke="#666" stroke-width="1.5"/>
<polygon points="200,96 195,88 205,88" fill="#666"/>
<!-- PreToolUse -->
<rect x="60" y="98" width="280" height="56" rx="6" fill="#e8f8f0" stroke="#1e8449" stroke-width="1.5"/>
<text x="70" y="116" fill="#1e8449" font-size="11" font-weight="bold">PreToolUse — policy gate</text>
<text x="70" y="131" fill="#196f3d" font-size="10">.botignore blocks reads, writes, commands</text>
<text x="70" y="144" fill="#196f3d" font-size="10">Blocked? Operation never executes.</text>
<!-- Block branch right — DENIED box -->
<line x1="340" y1="126" x2="370" y2="126" stroke="#c0392b" stroke-width="1.5"/>
<polygon points="374,126 366,121 366,131" fill="#c0392b"/>
<rect x="378" y="112" width="100" height="28" rx="4" fill="#fdecea" stroke="#c0392b" stroke-width="1"/>
<text x="428" y="130" text-anchor="middle" font-size="10" font-weight="bold" fill="#c0392b">DENIED</text>
<!-- Arrow down (allowed) -->
<line x1="200" y1="154" x2="200" y2="170" stroke="#1e8449" stroke-width="1.5"/>
<polygon points="200,174 195,166 205,166" fill="#1e8449"/>
<text x="215" y="168" fill="#1e8449" font-size="9">allowed</text>
<!-- Tool executes -->
<rect x="60" y="176" width="280" height="32" rx="6" fill="#fff" stroke="#636e72" stroke-width="1"/>
<text x="200" y="196" text-anchor="middle" fill="#2d3436" font-size="11">Tool executes (Read, Bash, Edit, Write)</text>
<!-- Arrow down -->
<line x1="200" y1="208" x2="200" y2="224" stroke="#666" stroke-width="1.5"/>
<polygon points="200,228 195,220 205,220" fill="#666"/>
<!-- PostToolUse -->
<rect x="60" y="230" width="280" height="70" rx="6" fill="#e8f0f8" stroke="#2471a3" stroke-width="1.5"/>
<text x="70" y="248" fill="#2471a3" font-size="11" font-weight="bold">PostToolUse — secret redaction</text>
<text x="70" y="263" fill="#1a5276" font-size="10">.botsecrets known values → *****</text>
<text x="70" y="278" fill="#1a5276" font-size="10">Heuristic patterns → flagged/redacted</text>
<text x="70" y="291" fill="#1a5276" font-size="9">Aho-Corasick automaton, sub-millisecond</text>
<!-- Arrow down -->
<line x1="200" y1="300" x2="200" y2="316" stroke="#666" stroke-width="1.5"/>
<polygon points="200,320 195,312 205,312" fill="#666"/>
<!-- Clean output -->
<rect x="60" y="322" width="280" height="32" rx="6" fill="#dfe6e9" stroke="#636e72" stroke-width="1"/>
<text x="200" y="342" text-anchor="middle" fill="#2d3436" font-size="11" font-weight="600">Clean output enters LLM context</text>
<!-- Right column: examples — aligned to flow steps, below DENIED box -->
<text x="500" y="62" text-anchor="middle" fill="#636e72" font-size="10" font-weight="600">WHAT GETS CAUGHT</text>
<!-- PreToolUse examples — starts below DENIED box -->
<rect x="490" y="74" width="200" height="72" rx="6" fill="#e8f8f0" stroke="#1e8449" stroke-width="1" stroke-dasharray="4,2"/>
<text x="500" y="91" fill="#1e8449" font-size="10" font-weight="600">PreToolUse</text>
<text x="500" y="106" fill="#333" font-size="9">Read .env → blocked</text>
<text x="500" y="119" fill="#333" font-size="9">rm -rf / → denied</text>
<text x="500" y="132" fill="#333" font-size="9">cat secrets/db.env → blocked</text>
<!-- PostToolUse examples — aligned with PostToolUse box -->
<rect x="390" y="230" width="300" height="70" rx="6" fill="#e8f0f8" stroke="#2471a3" stroke-width="1" stroke-dasharray="4,2"/>
<text x="400" y="247" fill="#2471a3" font-size="10" font-weight="600">PostToolUse</text>
<text x="400" y="262" fill="#333" font-size="10">DB_PASSWORD=hunter2 → DB_PASSWORD=*****</text>
<text x="400" y="275" fill="#333" font-size="10">AKIA1234567890ABCDEF → ***** (heuristic)</text>
<text x="400" y="288" fill="#333" font-size="10">docker-compose config → 2 values scrubbed</text>
<!-- Beyond fermata — aligned with clean output -->
<rect x="390" y="322" width="300" height="32" rx="6" fill="#fdecea" stroke="#c0392b" stroke-width="1" stroke-dasharray="4,2"/>
<text x="400" y="342" fill="#c0392b" font-size="10" font-weight="600">Beyond fermata — </text>
<text x="510" y="342" fill="#a93226" font-size="9">network exfil, kernel access → sandbox</text>
<!-- Performance -->
<rect x="390" y="170" width="300" height="42" rx="6" fill="#fff" stroke="#ddd" stroke-width="1"/>
<text x="400" y="187" fill="#333" font-size="10" font-weight="600">Performance</text>
<text x="400" y="202" fill="#666" font-size="10">~1-5ms per tool call. Cold start ~10-20ms.</text>
<!-- Key insight box -->
<rect x="60" y="375" width="630" height="44" rx="6" fill="#fff3cd" stroke="#d4a017" stroke-width="1"/>
<text x="75" y="393" fill="#7d6608" font-size="10" font-weight="bold">Key insight</text>
<text x="75" y="409" fill="#7d6608" font-size="10">source .env &amp;&amp; echo $DB_PASSWORD is caught — no file read was blocked, but the secret value is scrubbed from output.</text>
<!-- Delivery modes -->
<rect x="60" y="430" width="630" height="38" rx="6" fill="#fff" stroke="#ddd" stroke-width="1"/>
<text x="75" y="448" fill="#333" font-size="10" font-weight="600">Same engine, different wiring:</text>
<text x="265" y="448" fill="#666" font-size="10">Hook script (Claude Code, Codex) · MCP proxy (planned) · Library API (in-process)</text>
</svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

+71
View File
@@ -0,0 +1,71 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 520" width="720" height="520" font-family="system-ui, -apple-system, sans-serif" font-size="11">
<rect x="0" y="0" width="720" height="520" rx="8" fill="#f8f9fa"/>
<text x="360" y="24" text-anchor="middle" font-size="13" font-weight="bold" fill="#1a1a2e">The Reveal Triangle — why blocking the file is not enough</text>
<!-- Three dimensions -->
<text x="30" y="52" font-size="10" font-weight="bold" fill="#636e72">THREE INDEPENDENT DIMENSIONS</text>
<!-- Read -->
<rect x="30" y="62" width="210" height="68" rx="6" fill="#e8f8f0" stroke="#1e8449" stroke-width="1.5"/>
<text x="40" y="80" font-size="11" font-weight="bold" fill="#1e8449">Read</text>
<text x="40" y="94" font-size="9" fill="#333">Can the agent open a file?</text>
<text x="40" y="107" font-size="9" fill="#888">Operates on file identity</text>
<text x="40" y="120" font-size="9" fill="#1e8449">→ .botignore handles this</text>
<!-- Write -->
<rect x="255" y="62" width="210" height="68" rx="6" fill="#e8f0f8" stroke="#2471a3" stroke-width="1.5"/>
<text x="265" y="80" font-size="11" font-weight="bold" fill="#2471a3">Write</text>
<text x="265" y="94" font-size="9" fill="#333">Can the agent modify a file?</text>
<text x="265" y="107" font-size="9" fill="#888">Operates on file identity</text>
<text x="265" y="120" font-size="9" fill="#2471a3">→ git provides recovery</text>
<!-- Reveal -->
<rect x="480" y="62" width="210" height="68" rx="6" fill="#fdecea" stroke="#c0392b" stroke-width="2"/>
<text x="490" y="80" font-size="11" font-weight="bold" fill="#c0392b">Reveal</text>
<text x="490" y="94" font-size="9" fill="#333">Do secret values enter the LLM?</text>
<text x="490" y="107" font-size="9" fill="#888">Operates on data content</text>
<text x="490" y="120" font-size="9" fill="#c0392b">→ nothing addresses this today</text>
<!-- Independence callout -->
<rect x="30" y="142" width="660" height="24" rx="4" fill="#fff3cd" stroke="#d4a017" stroke-width="1"/>
<text x="360" y="158" text-anchor="middle" font-size="10" fill="#7d6608">These are independent. An agent can have read access to .env without secret values being revealed — if output is redacted.</text>
<!-- How secrets reach the LLM despite file blocking -->
<text x="30" y="190" font-size="10" font-weight="bold" fill="#636e72">HOW SECRETS REACH THE LLM DESPITE FILE BLOCKING</text>
<text x="430" y="190" font-size="9" fill="#888">(from agent behavior research)</text>
<!-- Sophistication gradient -->
<!-- L0: Obvious -->
<rect x="30" y="200" width="330" height="44" rx="6" fill="#e8f8f0" stroke="#1e8449" stroke-width="1"/>
<text x="40" y="216" font-size="10" font-weight="bold" fill="#1e8449">Obvious — caught by file ACL</text>
<text x="40" y="232" font-size="9" fill="#333">cat .env · Read(".env") · cat ~/.aws/credentials</text>
<rect x="370" y="200" width="320" height="44" rx="6" fill="#e8f8f0" stroke="#1e8449" stroke-width="1" stroke-dasharray="4,2"/>
<text x="380" y="216" font-size="9" fill="#1e8449">Blocked by .botignore</text>
<text x="380" y="232" font-size="9" fill="#888">The agent names the file directly. ~90% of cases.</text>
<!-- L2-L3: Indirect value exposure -->
<rect x="30" y="254" width="330" height="56" rx="6" fill="#fef5e7" stroke="#d4a017" stroke-width="1"/>
<text x="40" y="270" font-size="10" font-weight="bold" fill="#7d6608">Indirect — values leak through other tools</text>
<text x="40" y="284" font-size="9" fill="#333">docker-compose config (interpolates .env values)</text>
<text x="40" y="298" font-size="9" fill="#333">printenv · env (shows sourced env vars)</text>
<rect x="370" y="254" width="320" height="56" rx="6" fill="#fdecea" stroke="#c0392b" stroke-width="1" stroke-dasharray="4,2"/>
<text x="380" y="270" font-size="9" fill="#c0392b">File was never read — values still leak</text>
<text x="380" y="284" font-size="9" fill="#888">The secret appears in tool output from a command</text>
<text x="380" y="298" font-size="9" fill="#888">that has nothing to do with the secret file.</text>
<!-- L4: Constructed access -->
<rect x="30" y="320" width="330" height="56" rx="6" fill="#fef5e7" stroke="#d4a017" stroke-width="1"/>
<text x="40" y="336" font-size="10" font-weight="bold" fill="#7d6608">Constructed — agent builds the path indirectly</text>
<text x="40" y="350" font-size="9" fill="#333">cat $CONFIG_FILE · cat $(find . -name '.env*')</text>
<text x="40" y="364" font-size="9" fill="#333">writes a script that reads .env, then runs it</text>
<rect x="370" y="320" width="320" height="56" rx="6" fill="#fdecea" stroke="#c0392b" stroke-width="1" stroke-dasharray="4,2"/>
<text x="380" y="336" font-size="9" fill="#c0392b">Path is hidden from static analysis</text>
<text x="380" y="350" font-size="9" fill="#888">Variable expansion, command substitution, or a</text>
<text x="380" y="364" font-size="9" fill="#888">two-step write-then-execute pattern.</text>
<!-- L6: Exfiltration -->
<rect x="30" y="386" width="330" height="44" rx="6" fill="#f5eef8" stroke="#7f8c8d" stroke-width="1"/>
<text x="40" y="402" font-size="10" font-weight="bold" fill="#7f8c8d">Exfiltration — beyond tool-level defense</text>
<text x="40" y="418" font-size="9" fill="#333">curl -d "$(cat .env)" · git push with secrets</text>
<rect x="370" y="386" width="320" height="44" rx="6" fill="#f0eff4" stroke="#7f8c8d" stroke-width="1" stroke-dasharray="4,2"/>
<text x="380" y="402" font-size="9" fill="#7f8c8d">Needs sandbox / network controls</text>
<text x="380" y="418" font-size="9" fill="#888">Out of scope by design. See sandcage.</text>
<!-- What fermata covers -->
<rect x="30" y="444" width="660" height="64" rx="6" fill="#fff" stroke="#1e8449" stroke-width="1.5"/>
<text x="40" y="462" font-size="10" font-weight="bold" fill="#1a1a2e">fermata's coverage</text>
<rect x="40" y="470" width="10" height="10" rx="2" fill="#e8f8f0" stroke="#1e8449"/>
<text x="56" y="480" font-size="9" fill="#333">PreToolUse — blocks obvious file access (row 1)</text>
<rect x="280" y="470" width="10" height="10" rx="2" fill="#e8f0f8" stroke="#2471a3"/>
<text x="296" y="480" font-size="9" fill="#333">PostToolUse — redacts secret values from all output (rows 13)</text>
<text x="40" y="498" font-size="9" fill="#888">File ACLs solve Read. Secret redaction solves Reveal. Both are needed because they are independent dimensions.</text>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB