🏗️ fermata: redaction-first security model, unified .botsecrets config

Realign fermata around redaction (PostToolUse) as the primary security
layer, with access control (PreToolUse) as supplementary write/bash
protection. Remove botignore.toml — policy rules now live in .botsecrets
[policy] section. Add fermata.toml as an alias for .botsecrets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 01:10:07 +02:00
parent 77520819f6
commit 168aefd415
17 changed files with 571 additions and 423 deletions
+24 -6
View File
@@ -6,7 +6,7 @@ Fermata is a policy gate and secret filtering engine for AI coding agents. It sh
## fermata check
Check whether one or more paths are allowed for a given operation. Fermata locates the nearest project root (a directory containing `.botignore`, `botignore.toml`, or `.git`) and evaluates the policy defined there.
Check whether one or more paths are allowed for a given operation. Fermata locates the nearest project root (a directory containing `.botsecrets`, `.botignore`, or `.git`) and evaluates the policy defined there.
**Usage**
@@ -79,7 +79,7 @@ Fermata follows a **fail-open** policy for hooks: if the payload cannot be parse
**Event types**
- **pre-tool-use** -- Runs before the tool executes. Fermata checks the requested path or command against `.botignore` / `botignore.toml` and returns an allow, deny, or ask decision to the harness.
- **pre-tool-use** -- Runs before the tool executes. Fermata checks the requested path or command against `.botsecrets [policy]` / `.botignore` and returns an allow, deny, or ask decision to the harness.
- **post-tool-use** -- Runs after the tool executes. Fermata loads `.botsecrets`, builds a manifest of known secret values, and redacts any matches from the tool output before it enters the LLM context. A heuristic scanner (regex patterns derived from gitleaks) catches undeclared secrets as a safety net.
**Examples**
@@ -102,9 +102,8 @@ Fermata does not have its own global config file. All configuration lives in you
| File | Purpose |
|------|---------|
| `.botsecrets` | TOML file for secret redaction (`[files]`, `[heuristic]`, `[keys]`) **and** access-control policy (`[policy]` section with `[policy.read]`, `[policy.write]`, `[policy.bash]`). Unified config for most projects. `fermata.toml` is accepted as an alias (same format, `.botsecrets` takes priority). |
| `.botignore` | Gitignore-syntax patterns. Blocks both reads and writes to matched paths. |
| `botignore.toml` | Per-operation rules with `[read]`, `[write]`, and `[bash]` sections. |
| `.botsecrets` | TOML file declaring which files contain secrets, custom key patterns, and heuristic scanning options. |
Fermata discovers these files by walking up from the target path (or the current working directory for `hook`) until it finds a project root.
@@ -135,6 +134,25 @@ enabled = true
The `.botignore` blocks direct reads. The `.botsecrets` catches secrets that leak through indirect paths (shell output, log files, error messages).
### Single-file setup with .botsecrets
You can consolidate both secret redaction and access policy into `.botsecrets` alone, replacing the need for a separate `.botignore`:
```toml
# .botsecrets
[files]
patterns = [".env", ".env.*"]
[heuristic]
enabled = true
[policy.read]
deny = [".env", ".env.*", "secrets/**", "credentials/**"]
[policy.bash]
deny = ["rm -rf /", "curl * | sh"]
```
### Wire fermata into Claude Code
Add both hook events to `.claude/settings.json`:
@@ -164,10 +182,10 @@ Add both hook events to `.claude/settings.json`:
### Block dangerous shell commands
Use `botignore.toml` to deny specific command patterns:
Use `.botsecrets [policy.bash]` to deny specific command patterns:
```toml
[bash]
[policy.bash]
deny = ["rm -rf /", "curl * | sh", "wget * | bash"]
```
+158 -214
View File
@@ -1,165 +1,29 @@
# Configuration Reference
Fermata uses three configuration files to control what an AI coding agent can
access and what secret values it can see. Configuration is layered, with later
(more specific) sources overriding earlier ones.
Fermata uses up to two configuration files to control what an AI coding agent
can access and what secret values it can see. `.botsecrets` is the primary
configuration file. Most projects need only this file.
| File | Purpose | Syntax |
|------|---------|--------|
| `.botignore` | Block agent access to matching paths (reads and writes) | gitignore |
| `botignore.toml` | Fine-grained per-operation rules (read, write, bash) | TOML |
| `.botsecrets` | Declare secret-containing files and control redaction | TOML |
| `.botsecrets` | Declare secrets, control redaction, and set policy rules | TOML |
| `.botignore` | Block agent access to matching paths (optional, gitignore syntax) | gitignore |
All three files are optional. Without any configuration Fermata allows all
operations and performs no redaction. Add only the files you need.
`fermata.toml` is accepted as an alias for `.botsecrets` (same format, `.botsecrets` takes priority when both exist).
Configuration is layered, with later (more specific) sources overriding earlier
ones. All files are optional. Without any configuration Fermata allows all
operations and performs no redaction.
---
## `.botignore` -- Path-Based Access Control
## `.botsecrets` -- Secret Redaction and Policy
A `.botignore` file uses **gitignore syntax** to block agent access to matching
paths. When a path matches, both reads and writes are denied. There is no
distinction between operations -- if you need per-operation control, use
`botignore.toml` instead.
### Placement and Scoping
`.botignore` files can be placed at any level of your project directory tree.
Fermata walks the project root recursively and loads every `.botignore` it
finds. Each file is scoped to its own directory:
```
myproject/
.botignore # applies to the entire project
infra/
.botignore # applies only under infra/
src/
.botignore # applies only under src/
```
When multiple `.botignore` files match the same path, the **deepest
(most specific) file wins**. A negation pattern (`!`) at any depth overrides
an ignore from a shallower `.botignore`.
### Syntax
Standard gitignore rules apply:
- Blank lines and lines starting with `#` are ignored.
- `*` matches anything except `/`.
- `**` matches zero or more directories.
- A trailing `/` matches directories only.
- A leading `/` anchors the pattern to the `.botignore` file's directory.
- Prefix a pattern with `!` to negate (whitelist) a previously ignored path.
### Example
```gitignore
# Block all environment files
.env
.env.*
# Block the entire secrets directory
secrets/**
# Block private keys
*.pem
*.key
id_rsa
id_ed25519
# But allow the public key
!id_ed25519.pub
```
---
## `botignore.toml` -- Per-Operation Rules
`botignore.toml` provides fine-grained control by separating rules into three
namespaces: `[read]`, `[write]`, and `[bash]`. Place this file at the project
root alongside `.botignore`.
When both `.botignore` and `botignore.toml` are present, they are evaluated
together. `.botignore` is checked first (blocking both reads and writes), then
`botignore.toml` namespace-specific rules are applied. A path blocked by either
source is denied.
### `[read]` -- Read Access Rules
Blocks the agent from reading matching paths. Patterns use glob syntax.
| Field | Type | Description |
|-------|------|-------------|
| `patterns` | `string[]` | Glob patterns for paths the agent cannot read. |
```toml
[read]
patterns = [".env*", "secrets/**", "*.pem"]
```
### `[write]` -- Write Access Rules
Blocks the agent from writing to matching paths. Patterns use glob syntax.
| Field | Type | Description |
|-------|------|-------------|
| `patterns` | `string[]` | Glob patterns for paths the agent cannot write to. |
```toml
[write]
patterns = ["vendor/**", "*.lock", "migrations/**"]
```
### `[bash]` -- Command Execution Rules
Controls which shell commands the agent can run. Commands are evaluated in
priority order: deny, then allow_prefixes, then ask. If none match, the
command is allowed.
| Field | Type | Description |
|-------|------|-------------|
| `deny` | `string[]` | Patterns that block commands outright. Substring match by default; glob metacharacters (`*`, `?`, `[`) enable glob matching. |
| `allow_prefixes` | `string[]` | Command prefixes that are always allowed. If a command starts with a prefix, it passes immediately (before `ask` is checked). Trailing `:*` is stripped before matching. |
| `ask` | `string[]` | Patterns that require user confirmation before executing. Same matching rules as `deny`. |
Evaluation order:
1. If any `deny` pattern matches, the command is **blocked**.
2. If any `allow_prefixes` entry matches, the command is **allowed**.
3. If any `ask` pattern matches, the command **requires confirmation**.
4. Otherwise the command is **allowed**.
```toml
[bash]
deny = ["rm -rf /", "curl * | sh", ":(){ :|:& };:"]
allow_prefixes = ["cargo", "npm", "git status"]
ask = ["docker", "kubectl"]
```
### Full `botignore.toml` Example
```toml
[read]
patterns = [".env*", "secrets/**"]
[write]
patterns = ["vendor/**", "*.lock", "dist/**"]
[bash]
deny = ["rm -rf /", "curl * | sh"]
allow_prefixes = ["cargo", "npm", "just"]
ask = ["docker", "kubectl", "terraform"]
```
---
## `.botsecrets` -- Secret Redaction
`.botsecrets` declares which files contain secrets and how Fermata should
redact them from tool output. This prevents secret values from leaking into
the LLM context window even when the agent reads files indirectly (via shell
output, log files, error messages, etc.).
`.botsecrets` declares which files contain secrets, how Fermata should
redact them from tool output, and (optionally) access control policy rules.
This prevents secret values from leaking into the LLM context window even
when the agent reads files indirectly (via shell output, log files, error
messages, etc.).
### Layered Configuration
@@ -341,35 +205,126 @@ path = ".env.production"
format = "env"
```
### `[policy]` -- Access Control Rules
Optional access control rules embedded directly in `.botsecrets`.
#### `[policy.write]` -- Write Protection
Blocks the agent from writing to matching paths. This is the primary use case
for access control -- protecting vendored code, lock files, and policy files
from modification.
| Field | Type | Description |
|-------|------|-------------|
| `patterns` | `string[]` | Glob patterns for paths the agent cannot write to. |
```toml
[policy.write]
patterns = [".claude/**", "vendor/**", "*.lock", "dist/**"]
```
#### `[policy.bash]` -- Command Execution Rules
Controls which shell commands the agent can run.
| Field | Type | Description |
|-------|------|-------------|
| `deny` | `string[]` | Patterns that block commands outright. |
| `allow_prefixes` | `string[]` | Command prefixes always allowed. |
| `ask` | `string[]` | Patterns requiring user confirmation. |
```toml
[policy.bash]
deny = ["rm -rf /", "curl * | sh"]
allow_prefixes = ["cargo", "npm", "just"]
ask = ["docker", "kubectl"]
```
#### `[policy.read]` -- Read Restrictions (Rarely Needed)
Blocks reads of matching paths. Rarely needed -- secret values are redacted by
the PostToolUse layer regardless of whether the read is allowed. Use this only
for files the agent cannot usefully read (e.g., binary blobs, large data files).
```toml
[policy.read]
patterns = ["*.sqlite", "*.dat"]
```
---
## `.botignore` -- Path-Based Access Control
For most projects, `.botsecrets` with `[policy]` is sufficient. `.botignore`
remains useful for monorepo subtree exclusion or teams that prefer gitignore
syntax for simple path blocking.
A `.botignore` file uses **gitignore syntax** to block agent access to matching
paths. When a path matches, both reads and writes are denied. There is no
distinction between operations -- if you need per-operation control, use
the `[policy]` section in `.botsecrets` instead.
### Placement and Scoping
`.botignore` files can be placed at any level of your project directory tree.
Fermata walks the project root recursively and loads every `.botignore` it
finds. Each file is scoped to its own directory:
```
myproject/
.botignore # applies to the entire project
infra/
.botignore # applies only under infra/
src/
.botignore # applies only under src/
```
When multiple `.botignore` files match the same path, the **deepest
(most specific) file wins**. A negation pattern (`!`) at any depth overrides
an ignore from a shallower `.botignore`.
### Syntax
Standard gitignore rules apply:
- Blank lines and lines starting with `#` are ignored.
- `*` matches anything except `/`.
- `**` matches zero or more directories.
- A trailing `/` matches directories only.
- A leading `/` anchors the pattern to the `.botignore` file's directory.
- Prefix a pattern with `!` to negate (whitelist) a previously ignored path.
### Example
```gitignore
# Block entire subtrees in a monorepo
packages/legacy-app/
vendor/
# Block private keys
*.pem
*.key
id_rsa
id_ed25519
# But allow the public key
!id_ed25519.pub
```
---
## Examples
### Minimal: Just Block Sensitive Files
### Minimal: Just Redact Secrets (most projects)
If you only need to prevent the agent from reading certain files, a single
`.botignore` is enough:
```gitignore
# .botignore
.env
.env.*
*.pem
*.key
```toml
# .botsecrets
[files]
patterns = [".env", ".env.*"]
```
### Standard: Block Access and Redact Secrets
The most common setup. Block direct access with `.botignore` and redact leaked
values with `.botsecrets`:
```gitignore
# .botignore
.env
.env.*
secrets/**
*.pem
```
### Standard: Redact Secrets + Write Protection
```toml
# .botsecrets
@@ -379,56 +334,34 @@ patterns = [".env", ".env.*", "secrets/*.yaml"]
[keys]
include = ["STRIPE_*"]
[redaction]
style = "masked"
[policy.write]
patterns = [".claude/**", "vendor/**", "*.lock"]
[policy.bash]
deny = ["rm -rf /", "curl * | sh"]
```
### Fine-Grained: Separate Read, Write, and Bash Rules
### With .botignore: Simple Path Blocking + Redaction
Use `botignore.toml` when you need different rules for different operations:
```toml
# botignore.toml
[read]
patterns = [".env*", "secrets/**"]
[write]
patterns = ["vendor/**", "*.lock", "Cargo.toml"]
[bash]
deny = ["rm -rf *"]
allow_prefixes = ["cargo", "npm", "git"]
ask = ["docker", "kubectl"]
```
### Full-Featured: All Three Files
A production setup using all configuration files together:
For teams that prefer gitignore syntax:
```gitignore
# .botignore
.env
.env.*
*.pem
*.key
id_rsa
id_ed25519
# .botignore (optional)
node_modules/
build/
dist/
```
```toml
# botignore.toml
[read]
patterns = ["secrets/**", ".aws/**"]
[write]
patterns = ["vendor/**", "*.lock", "dist/**", "migrations/**"]
[bash]
deny = ["rm -rf /", "curl * | sh"]
allow_prefixes = ["cargo", "npm", "just", "git"]
ask = ["docker", "terraform"]
# .botsecrets
[files]
patterns = [".env", ".env.*"]
```
### Full-Featured: Complete Configuration
A production setup using `.botsecrets` for both redaction and policy:
```toml
# .botsecrets
[files]
@@ -449,6 +382,17 @@ mode = "enforce"
mode = "strict"
on_parse_error = "mask-entire-file"
[policy.write]
patterns = [".claude/**", "vendor/**", "*.lock", "dist/**", "migrations/**"]
[policy.bash]
deny = ["rm -rf /", "curl * | sh"]
allow_prefixes = ["cargo", "npm", "just", "git"]
ask = ["docker", "terraform"]
[policy.read]
patterns = ["*.sqlite", "*.dat"]
[[file]]
path = "config/credentials.yaml"
format = "yaml"
+26 -17
View File
@@ -12,49 +12,49 @@ No AI coding agent ships built-in post-read secret filtering today. The entire i
Most security models think about secrets in terms of *files*: can the agent read this file? Can it write to it? fermata introduces a third dimension that traditional models miss entirely.
**Read** -- Can the agent open a file? Handled by policy rules and file access controls. A blunt instrument: blocking `.env` also blocks legitimate tooling that needs configuration values.
**Read** -- Can the agent open a file? Handled by policy rules and file access controls. A blunt instrument: blocking `.env` also blocks legitimate tooling that needs configuration values. In fermata's model, read access to secret-containing files is *allowed by default* -- the agent needs to see file structure and key names to reason about configuration.
**Write** -- Can the agent modify a file? Less concerning than it appears, because version control provides full recovery. Write restrictions matter primarily for anti-jailbreak (preventing the agent from modifying its own hooks or policy files).
**Reveal** -- Do secret *values* enter the LLM context? This is the novel concern. No traditional security model addresses it because the concept did not exist before AI agents. A file read that configures a database is not a security event. The same file read that feeds credentials to an LLM *is*.
These three dimensions are independent. An agent can have read access to `.env` without the secret values being revealed -- if the output is redacted before it reaches the model. This is the target state: the agent sees file structure, knows which keys exist, can reason about configuration, but never sees actual secret values.
These three dimensions are independent. An agent can have read access to `.env` without the secret values being revealed -- the output is redacted before it reaches the model. This is the default state, not a special configuration: the agent sees file structure, knows which keys exist, can reason about configuration, but never sees actual secret values.
The key insight is that Read and Write operate on file identity (*which file*). Reveal operates on data content (*which values*). The reveal problem can only be solved at the data-content level. File identity is necessary but not sufficient.
## Defense in Depth
fermata implements a layered security stack. Each layer catches what the layers above it miss.
fermata implements a layered security stack. Layers 1 and 2 are the primary defense -- they operate on data content (the actual secret values). Layers 3 and 4 are supplementary -- they operate on file and system identity.
### Layer 1: Access Control (.botignore)
Block direct operations on sensitive files. `Read .env` -- denied. `Bash: rm -rf /` -- denied.
This is the 90% mistake avoider. It catches obvious agent operations on files that policy says are off-limits. It uses gitignore-style patterns, so the syntax is already familiar.
**Limitation**: Cannot catch indirect access. An agent running `source .env && echo $DB_PASSWORD` bypasses file-level controls entirely. This is why access control alone is not enough.
### Layer 2: Known-Value Redaction (.botsecrets + Aho-Corasick)
### Layer 1: Known-Value Redaction (.botsecrets + Aho-Corasick)
Parse secret-containing files at startup, extract actual secret values, build an Aho-Corasick automaton. Scan all tool output for those exact byte strings and replace them with redaction markers.
This catches secrets regardless of how they appear in output -- direct file reads, shell command output, log files, error messages. If the value `hunter2` is a declared secret, every occurrence in every tool output is redacted before it reaches the model.
This is the primary defense layer. It catches secrets regardless of how they appear in output -- direct file reads, shell command output, log files, error messages. If the value `hunter2` is a declared secret, every occurrence in every tool output is redacted before it reaches the model.
**Guarantees**: Zero false negatives for declared secrets. Sub-millisecond per scan. The Aho-Corasick automaton finds all occurrences in a single linear pass over the output.
### Layer 3: Heuristic Detection (Scanner + gitleaks patterns)
### Layer 2: Heuristic Detection (Scanner + gitleaks patterns)
Regex patterns for known secret formats: AWS access keys (`AKIA...`), GitHub PATs (`ghp_...`), JWTs (`eyJ...`), database URLs with embedded passwords, and dozens more derived from industry-standard pattern sets.
This is the safety net for secrets not covered by the manifest -- secrets in files that `.botsecrets` does not know about, secrets generated at runtime, secrets that appear in unexpected places.
Higher false-positive rate than Layer 2, but as a secondary safety net, that tradeoff is acceptable.
Higher false-positive rate than Layer 1, but as a secondary safety net, that tradeoff is acceptable.
### Layer 3: Access Control (.botignore / .botsecrets [policy])
Block dangerous write operations and destructive commands. `Write .claude/settings.json` -- denied. `Bash: rm -rf /` -- denied. Uses gitignore-style patterns for write protection and command deny-lists for bash safety.
This layer is supplementary -- it protects against destructive writes and dangerous commands, not secret reads. Reading `.env` is allowed; the secret values never reach the model because Layer 1 redacts them. Write restrictions remain important for anti-jailbreak (preventing the agent from modifying its own hooks or policy files) and for protecting vendored or generated files.
**Limitation**: Cannot catch indirect access to secret values. An agent running `source .env && echo $DB_PASSWORD` bypasses file-level controls entirely. This is exactly why access control is supplementary and redaction (Layer 1) is primary.
### Layer 4: Structural Containment (External)
Container-level isolation, filesystem restrictions, dropped capabilities, no-new-privileges. Prevents system modification, privilege escalation, and escape from the execution environment. This layer has no concept of secret values -- it operates on structural boundaries.
fermata does not own this layer. It is provided by your container runtime, VM, or sandboxing tool. fermata's design assumes that structural containment exists as the outermost boundary, and focuses on the data-content layers (2 and 3) that containment cannot address.
fermata does not own this layer. It is provided by your container runtime, VM, or sandboxing tool. fermata's design assumes that structural containment exists as the outermost boundary, and focuses on the data-content layers (1 and 2) that containment cannot address.
## Design Principles
@@ -66,7 +66,7 @@ fermata does not own this layer. It is provided by your container runtime, VM, o
**Harness-agnostic.** fermata does not assume any specific AI coding agent. The policy engine is pure logic. Harness adapters are thin translation layers. The `.botsecrets` format works identically whether fermata runs as a hook script, an MCP proxy, or an in-process library.
**Single policy file.** `.botsecrets` is the user-facing interface. One file declares what to protect. How protection is delivered is a deployment concern, not a configuration concern.
**Single policy file.** `.botsecrets` is the unified configuration interface. One file declares both what to redact (`[files]`, `[keys]`, `[heuristic]`) and what to restrict (`[policy]`). How protection is delivered is a deployment concern, not a configuration concern.
## The .botsecrets Vision
@@ -89,6 +89,15 @@ include = ["STRIPE_*", "MY_APP_SIGNING_*"]
[heuristic]
enabled = true
# Access control: write protection and bash safety.
# Reading secret-containing files is allowed -- Layer 1 redacts the values.
[policy.write]
patterns = [".claude/**", "vendor/**", "*.lock"]
[policy.bash]
deny = ["rm -rf /", "curl * | sh"]
```
No secret values appear in `.botsecrets` itself. It points to the files that contain them. The secret extraction, automaton construction, and output scanning happen automatically. Built-in key patterns (`*_KEY`, `*_SECRET`, `*_PASSWORD`, `*_TOKEN`, `DATABASE_URL`, etc.) handle most projects without custom `[keys]` configuration.
+23 -16
View File
@@ -16,19 +16,34 @@ This shifts the design centre from "what can a smart attacker hide?" to "what do
Think of fermata as a guardrail on a mountain road, not an armoured wall. It catches the statistical case -- the distracted driver drifting toward the edge -- not the determined off-roader who drives around it.
Fermata's primary defence is not path blocking but **value redaction**. Even when a path-level check misses a read (L4 shell indirection, L5 derived names), known secret values are scrubbed from tool output before the LLM sees them. The path-level checks (L0--L3) provide supplementary protection -- they prevent unnecessary file access and dangerous commands, but they are not the last line of defence against secret leakage.
---
## What fermata catches
Fermata's detection is organised by how the agent names the path or secret it is about to access. Lower levels are easier to detect statically; higher levels require progressively more runtime information.
Fermata's detection operates on two independent tracks: **value-level redaction** (primary) and **path-level access control** (supplementary).
### Secret redaction (PostToolUse) -- primary defence
Independent of path-level detection, fermata filters tool output before it enters the LLM context:
- **Known-value redaction** -- `.botsecrets` declares which files contain secrets. Fermata parses those files, extracts the secret values, and replaces them in all tool output using an Aho-Corasick automaton. Sub-millisecond performance. Zero false negatives for declared secrets.
- **Heuristic scanning** -- regex patterns (derived from gitleaks) detect undeclared secrets in tool output: AWS access keys, JWTs, GitHub PATs, database connection strings, and similar high-entropy tokens. This is a safety net for secrets not covered by the manifest.
This is the layer that makes fermata's security model resilient. Even if every path-level check fails -- the agent reaches `.env` through a route fermata didn't parse -- the secret values themselves are scrubbed from the output before the LLM sees them.
### Path-level access control (PreToolUse) -- supplementary
The path-level checks below are organised by how the agent names the path it is about to access. Lower levels are easier to detect statically; higher levels require progressively more runtime information. These checks prevent unnecessary file access and reduce noise, but they are not the last line of defence.
### L0: Direct path arguments -- fully covered
The agent calls a path-typed tool (Read, Write, Edit) with an explicit file path. This is fermata's home turf.
The agent calls a path-typed tool (Read, Write, Edit) with an explicit file path.
> `Read({"file_path": "/home/user/.env"})` -- `.botignore` matches -- deny.
Both `.botignore` (gitignore semantics, deepest directory wins, negation patterns work as in git) and `botignore.toml` `[read]`/`[write]` glob lists are evaluated against the resolved path. This covers the overwhelming majority of accidental secret-file reads, because most agents use direct path tools as their primary file access method.
Both `.botignore` (gitignore semantics, deepest directory wins, negation patterns work as in git) and `.botsecrets [policy]` rules are evaluated against the resolved path. This covers the overwhelming majority of accidental secret-file reads, because most agents use direct path tools as their primary file access method.
### L1: Absolute paths inside Bash commands
@@ -56,15 +71,6 @@ Fermata handles this by checking whether the glob pattern in the command **overl
The limitation: fermata can tell whether a glob *could* hit a protected file, but not whether it *actually will* in the current directory state. For the heuristic threat model, "could hit" is the right bar.
### Secret redaction (PostToolUse)
Independent of path-level detection, fermata filters tool output before it enters the LLM context:
- **Known-value redaction** -- `.botsecrets` declares which files contain secrets. Fermata parses those files, extracts the secret values, and replaces them in all tool output using an Aho-Corasick automaton. Sub-millisecond performance. Zero false negatives for declared secrets.
- **Heuristic scanning** -- regex patterns (derived from gitleaks) detect undeclared secrets in tool output: AWS access keys, JWTs, GitHub PATs, database connection strings, and similar high-entropy tokens. This is a safety net for secrets not covered by the manifest.
Together these mean that even if a path-level check misses a read (the agent reaches `.env` through a route fermata didn't parse), the secret values themselves are scrubbed from the output before the LLM sees them.
---
## What fermata does NOT catch
@@ -105,6 +111,7 @@ This is not a failure -- it is a design boundary. Trying to solve L6 within a st
| Level | Description | Detection | Confidence |
|-------|-------------|-----------|------------|
| Secret redaction | Known-value + heuristic scanning | Aho-Corasick + regex | High (zero false negatives for declared secrets) |
| L0 | Direct path argument | Full policy check | High |
| L1 | Absolute path in Bash | Path extraction + policy | High |
| L2 | Bare filename in CWD | CWD resolution + policy | Medium |
@@ -113,7 +120,7 @@ This is not a failure -- it is a design boundary. Trying to solve L6 within a st
| L5 | Derived names | Collapses to L0 at call time | Depends on final call |
| L6 | Protocol exfiltration | Out of scope | N/A |
Secret redaction (PostToolUse) operates as a separate, independent layer. Even when path-level detection at L1-L4 misses a read, known-value redaction catches the secret values in the output.
Secret redaction is the primary defence layer. Path-level detection (L0--L3) provides supplementary access control; even when it misses, redaction catches the secret values in the output.
---
@@ -123,10 +130,10 @@ Fermata is one layer in a defence-in-depth stack. Here is what it is good at and
### Fermata alone gives you
- **Accidental secret exposure prevention.** The most common case: agent reads `.env`, `secrets.toml`, or a credential file because it matched a directory listing. Fermata blocks this at L0 with zero configuration beyond `.botignore`.
- **Known-secret scrubbing.** Even if a secret leaks through an indirect read, declared secrets are redacted from LLM context. The agent never "learns" the value.
- **Heuristic secret detection.** Undeclared secrets matching common formats (AWS keys, tokens, connection strings) are flagged in tool output.
- **Known-secret scrubbing.** Declared secrets (via `.botsecrets`) are redacted from all tool output, always. The agent never "learns" the value, regardless of how it reached the file.
- **Heuristic secret detection.** Undeclared secrets matching common formats (AWS keys, tokens, connection strings) are flagged in tool output. Built-in patterns handle most projects without any configuration.
- **Command guardrails.** Dangerous shell patterns (`rm -rf /`, `curl | sh`) are caught by configurable deny lists.
- **Write protection and access control.** Path-level checks (`.botignore`, `.botsecrets [policy]`) prevent unnecessary file access and unauthorised modifications at L0--L3.
### Combine fermata with