Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7d71108
v0.9.3: Rust 2024 edition, optional build features, SQLite + security…
codehippie1 May 29, 2026
440a200
Pricing overrides + signed cards, graceful degradation, login service
codehippie1 Jun 7, 2026
d51ee88
v0.9.4: pricing overrides + signed cards, graceful degradation, login…
codehippie1 Jun 7, 2026
00acef9
v0.9.5: status-line ribbon + Windows install-service fix
codehippie1 Jun 8, 2026
f52eeac
v0.9.6: burnwall watch — live cross-tool status ribbon
codehippie1 Jun 8, 2026
1dfd2c1
v0.9.7: exfil detection, security digest, MCP PoC corpus, evidence pack
codehippie1 Jun 8, 2026
14e2d15
Iter 1: trust + ROI surfaces (attestations, savings, heartbeat, SECUR…
codehippie1 Jun 8, 2026
8231c65
Iter 2: security depth (destructive cmds, evasion hardening, swarm bu…
codehippie1 Jun 8, 2026
fe84ecc
v0.9.8: savings, signed share card, sidecar, destructive/exfil detect…
codehippie1 Jun 8, 2026
1f06411
ci: regenerate release.yml for build attestations (dist generate)
codehippie1 Jun 8, 2026
e7c5bf3
v0.9.9: burnwall upgrade (alias self-upgrade)
codehippie1 Jun 8, 2026
007734f
upgrade: sweep leftover burnwall.exe.old on next launch (Windows self…
codehippie1 Jun 8, 2026
1a75455
v0.9.10: status-line auto-wiring in init + burnwall uninstall
codehippie1 Jun 8, 2026
bd90942
v0.9.11: subscription headroom, coverage transparency, rule-pack corp…
codehippie1 Jun 9, 2026
427c624
fix(install/ci): match real release artifacts + retry flaky attestation
codehippie1 Jun 9, 2026
80dbd29
v0.9.12: multi-shell routing sync, not-routed status warnings, colori…
codehippie1 Jun 9, 2026
b43492c
fix(ci): allow-dirty=[ci] so dist tolerates the patched release.yml
codehippie1 Jun 9, 2026
810803c
fix(security): scope command-shaped rules to tool-call arguments
codehippie1 Jun 9, 2026
bfb1c63
fix(cli): tie shell routing lifecycle to the proxy lifecycle
codehippie1 Jun 9, 2026
db9ad0f
fix(security): scope tool-arg scanning to the latest in-flight tool r…
codehippie1 Jun 10, 2026
f57d453
feat(pricing): add Claude Fable 5 + Opus 4.8; resolve [1m] variant tags
codehippie1 Jun 10, 2026
66d38da
fix(cli): uninstall deletes routing env files and warns about open sh…
codehippie1 Jun 10, 2026
092f01c
v0.9.13: prose-safe scanning, conversation recovery, routing lifecycl…
codehippie1 Jun 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
permissions:
"attestations": "write"
"contents": "read"
"id-token": "write"
steps:
- name: enable windows longpaths
run: |
Expand Down Expand Up @@ -144,6 +148,30 @@ jobs:
# Actually do builds and make zips and whatnot
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
echo "dist ran successfully"
# NOTE: manual patch over the cargo-dist-generated workflow — re-apply
# after `dist generate`. Retries build-provenance attestation up to 3x
# because Sigstore's transparency log intermittently returns a transient
# "InternalError: error fetching tlog entry". Attestation stays MANDATORY:
# the final attempt is not continue-on-error, so a persistent Sigstore
# outage still fails the job (we never ship an un-attested release).
- name: Attest
id: attest1
continue-on-error: true
uses: actions/attest@v4
with:
subject-path: "target/distrib/*${{ join(matrix.targets, ', ') }}*"
- name: Attest (retry 1)
id: attest2
if: steps.attest1.outcome == 'failure'
continue-on-error: true
uses: actions/attest@v4
with:
subject-path: "target/distrib/*${{ join(matrix.targets, ', ') }}*"
- name: Attest (retry 2)
if: steps.attest1.outcome == 'failure' && steps.attest2.outcome == 'failure'
uses: actions/attest@v4
with:
subject-path: "target/distrib/*${{ join(matrix.targets, ', ') }}*"
- id: cargo-dist
name: Post-build
# We force bash here just because github makes it really hard to get values up
Expand Down
45 changes: 45 additions & 0 deletions .github/workflows/scorecard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# OpenSSF Scorecard — supply-chain health signal for a zero-telemetry tool.
# A local tool can't use product analytics for trust; a published Scorecard +
# the dist-built reproducible release artifacts stand in for it.
name: Scorecard

on:
branch_protection_rule:
schedule:
- cron: "37 4 * * 1" # weekly, Monday
push:
branches: ["main"]

permissions: read-all

jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
security-events: write # upload SARIF to the Security tab
id-token: write # publish results to the public Scorecard API
steps:
- name: Checkout
uses: actions/checkout@v5
with:
persist-credentials: false

- name: Run analysis
uses: ossf/scorecard-action@v2.4.0
with:
results_file: results.sarif
results_format: sarif
publish_results: true

- name: Upload artifact
uses: actions/upload-artifact@v5
with:
name: scorecard-results
path: results.sarif
retention-days: 5

- name: Upload to code-scanning
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
401 changes: 401 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ src/
handler.rs — Request/response handler pipeline
forwarding.rs — Forward requests to upstream providers
streaming.rs — SSE/streaming response handling
cache_injection.rs — Optional Anthropic cache_control rewrite + savings projection
resilience.rs — Same-model endpoint failover + circuit breaking
providers/
mod.rs — Provider trait and registry
anthropic.rs — Anthropic Messages API parser
Expand Down Expand Up @@ -105,6 +107,7 @@ src/
config/
mod.rs — TOML config loading and defaults
types.rs — Config struct definitions
project.rs — Per-project .burnwall.yaml profile discovery + merge
cli/
mod.rs — CLI command definitions
start.rs — `burnwall start` command
Expand All @@ -113,12 +116,17 @@ src/
history.rs — `burnwall history` command
config_cmd.rs — `burnwall config` command (incl. `config doctor`)
init.rs — `burnwall init` (auto-detect + setup)
daemon.rs — Background spawn + liveness/PID-file (used by `start --daemon`/`stop`)
security.rs — `burnwall security` (rule inspection / scan testing)
completions.rs — `burnwall completions` (shell completion scripts)
mcp.rs / mcp_watch.rs — `burnwall mcp*` (approvals, audit export, watcher)
waste.rs / explore.rs / metrics.rs / digest.rs — insight + observability cmds
cost_per_pr.rs — `burnwall cost-per-pr` (git-attributed spend)
rules.rs — `burnwall rules` (install/add/test/sign/verify/fetch)
audit.rs / report.rs — `burnwall audit` (seal/verify/aibom/sarif) + `report`
observe/ — Local, metadata-only observability
metrics.rs / otel.rs / digest.rs — latency p50/p95, OTel span sink, AIBOM digest
attribution.rs — git branch/commit cost attribution
mcp/ — MCP firewall + multi-server watcher
mod.rs / firewall.rs — routing, tool-poisoning + rug-pull detection
audit/ — Cryptographic audit + compliance exports
Expand Down Expand Up @@ -197,7 +205,7 @@ Scan `tool_use` / `function_call` blocks in the REQUEST body (before forwarding)

## Important Notes for Claude Code Sessions

- Read `docs/SPEC.md` for exact CLI behavior and output formats
- Run `burnwall <cmd> --help` and read `README.md` for current CLI behavior and output formats
- Read `docs/ARCHITECTURE.md` for component design and data flow
- Work in focused, scoped sessions — one component at a time
- Write tests FIRST for any new parser or calculator logic
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 28 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
[package]
name = "burnwall"
version = "0.9.2"
edition = "2021"
version = "0.9.13"
edition = "2024"
rust-version = "1.87"
description = "Local proxy for AI coding tools (Claude Code, Codex CLI, Aider): cache-aware cost tracking, path/command security checks, daily budget enforcement. Zero telemetry."
# FSL-1.1-MIT is not an SPDX identifier; crates.io rejects it as `license`,
# so the license is declared via the file instead.
Expand All @@ -19,6 +20,27 @@ path-guid = "1B65F07B-49F5-469A-AF2C-8C091A57035A"
license = false
eula = false

# Optional feature clusters layered on top of the core proxy (cost + security
# + budget + storage). All on by default so the shipped binary is unchanged;
# `--no-default-features` builds the lean core. Implication edges mirror the
# module graph: audit→observe→logscrape and waste→logscrape.
[features]
default = ["audit", "mcp", "observe", "logscrape", "waste"]
logscrape = []
observe = ["logscrape"]
waste = ["logscrape"]
audit = ["observe"]
mcp = []

# Lint policy lives here (not as crate-wide `#![allow]`) so it is visible and
# reviewable. `unused` stays a warning rather than being silenced wholesale.
[lints.rust]
unused = "warn"
rust_2018_idioms = "warn"

[lints.clippy]
all = "warn"

[dependencies]
# Async runtime
tokio = { version = "1", features = ["full"] }
Expand Down Expand Up @@ -94,6 +116,10 @@ path = "tests/unit/parser_test.rs"
name = "pricing_test"
path = "tests/unit/pricing_test.rs"

[[test]]
name = "tls_integrity_test"
path = "tests/unit/tls_integrity_test.rs"

[[test]]
name = "storage_test"
path = "tests/unit/storage_test.rs"
Expand Down
64 changes: 57 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,10 @@ Works on macOS (arm64 + x86_64) and Linuxbrew.
Prebuilt archives for every release are at
<https://github.com/intbot/burnwall/releases>:

- `burnwall-aarch64-apple-darwin.tar.gz` — macOS Apple Silicon
- `burnwall-x86_64-apple-darwin.tar.gz` — macOS Intel
- `burnwall-x86_64-unknown-linux-gnu.tar.gz` — Linux x86_64
- `burnwall-aarch64-apple-darwin.tar.xz` — macOS Apple Silicon
- `burnwall-x86_64-apple-darwin.tar.xz` — macOS Intel
- `burnwall-aarch64-unknown-linux-gnu.tar.xz` — Linux arm64
- `burnwall-x86_64-unknown-linux-gnu.tar.xz` — Linux x86_64
- `burnwall-x86_64-pc-windows-msvc.zip` — Windows x86_64

Extract and put the `burnwall` binary anywhere on your `PATH`.
Expand All @@ -101,6 +102,24 @@ cargo install burnwall # from crates.io
git clone https://github.com/intbot/burnwall && cd burnwall && cargo build --release # from source
```

### Verify your download

Every release binary carries a GitHub Artifact Attestation (Sigstore keyless
build provenance, SLSA Build L2) — proof it was built from this repo's CI, not
swapped out. Verify before trusting a binary in your traffic path:

```bash
gh attestation verify burnwall-x86_64-unknown-linux-gnu.tar.xz --repo intbot/burnwall
```

Each release also ships per-file `.sha256` checksums and a combined `sha256.sum`:

```bash
sha256sum --ignore-missing -c sha256.sum
```

See [`SECURITY.md`](SECURITY.md) for the full integrity + TLS-handling statement.

## How It Works

Burnwall runs as a local HTTP proxy. You point your AI tools at it via environment variables:
Expand All @@ -123,18 +142,40 @@ Every API call flows through Burnwall:

Responses are **never modified** — Burnwall reads them, logs the cost, and passes them through unchanged.

### Defense-in-depth, not a silver bullet

Security rules are evaluated **before the request leaves your machine** — a
blocked request never reaches the provider. That's the point: it's another layer
that holds even when a tool's own approval prompt, allowlist, or sandbox is
bypassed (and those have been, repeatedly). Burnwall doesn't claim you're under
attack; it claims that *if* a prompt-injected agent tries to read `~/.ssh` or
pipe a secret to the network, the rule fires locally first. Pair it with your
tool's native controls — it's designed to complement them, not replace them.

## Scope: What Burnwall Guards

Burnwall sits on the **LLM API path** — the HTTP traffic between your AI tool and Anthropic/OpenAI. Security scanning, budget enforcement, and cost tracking all operate on that traffic.

It does **not** intercept **MCP** (Model Context Protocol) traffic. When your agent calls an MCP server's tools, that traffic flows through your AI tool directly — Burnwall never sees it, so it can't scan or block it. MCP-layer protection is a separate concern; dedicated MCP-firewall tools exist and run cleanly alongside Burnwall.
The LLM-path proxy does **not** automatically see **MCP** (Model Context Protocol) traffic — that flows from your AI tool to MCP servers directly. For that layer, Burnwall ships a dedicated **MCP firewall** you put in front of your MCP servers (`burnwall mcp-watch`): it detects tool-poisoning and "rug-pull" (silent post-approval redefinition) attacks and enforces an approval workflow. Run it alongside the main proxy for end-to-end coverage.

### The coverage boundary

Burnwall protects the traffic that **flows through it**. It does not man-in-the-middle TLS — it forwards via base-URL routing — so a tool that talks to a provider over a path the base URL can't redirect is simply not visible to it. By design, no proxy that avoids TLS interception can see that traffic.

In practice:

- **Routable, fully protected:** Claude Code (including on a Pro/Max subscription), Codex CLI in **API-key mode**, Aider, OpenCode, and other tools that honor `ANTHROPIC_BASE_URL` / `OPENAI_BASE_URL` or an equivalent API-base setting.
- **Not routable, bypasses entirely:** Codex CLI signed in with **ChatGPT login**, which talks to the ChatGPT backend over OAuth. Codex in **API-key mode** routes through Burnwall and can be protected — but it bills per-token instead of your flat subscription, so weigh the cost trade-off before switching.

So you're never left guessing, Burnwall tells you which of your installed tools are actually behind the firewall: `burnwall init` warns at setup if a tool is in a bypassing mode, and `burnwall status` (and `burnwall watch`) show a per-tool **Coverage** readout — *protected*, *installed but unseen*, or *bypasses*.

## Supported Tools

| Tool | Support | Configuration |
|------|---------|---------------|
| Claude Code | ✅ Full | `ANTHROPIC_BASE_URL` |
| Codex CLI (API key mode) | ✅ Full | `OPENAI_BASE_URL` |
| Codex CLI (ChatGPT login) | ❌ | Not interceptable (OAuth backend) |
| Aider | ✅ Full | `--openai-api-base` |
| OpenCode | ✅ Full | Settings |
| Cline | ✅ Full | Extension settings |
Expand Down Expand Up @@ -182,13 +223,22 @@ $ burnwall status
Cache savings today: $47.82
```

## Privacy
## Trust & privacy

Burnwall sits in your API traffic path, so it earns that position by being
verifiable, not by asking for trust:

- **100% local.** No data ever leaves your machine (except API forwarding).
- **100% local.** No data ever leaves your machine except the API forwarding you
asked for. Works offline (apart from the forwarding itself).
- **Zero telemetry.** No analytics, no phone-home, no tracking. Ever.
- **No prompt logging.** Only metadata is stored (model, tokens, cost, timestamp).
- **No API key storage.** Keys pass through in headers and are never written to disk.
- **Open source.** Audit the code yourself.
- **Read-only on responses.** Burnwall inspects responses to compute cost and
**never modifies them** — your tool gets the provider's bytes unchanged.
- **Single binary, signed releases.** Install from a checksummed, signed release
(or `cargo install` from source). No background services you didn't ask for.
- **Open source.** The "no network calls except forwarding" claim is auditable —
read the proxy code yourself.

## Terms of service

Expand Down
64 changes: 64 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Security

Burnwall sits in your AI API traffic path, so its own integrity matters as much
as the rules it enforces. This document states what we do to be verifiable, how
TLS is handled, and how to report a vulnerability.

## Reporting a vulnerability

Please report security issues privately via GitHub Security Advisories
("Report a vulnerability" on the repository's Security tab) rather than a public
issue. We aim to acknowledge within a few days.

## Self-integrity: verify what you run

- **Build provenance (SLSA Build L2).** Every released binary carries a GitHub
Artifact Attestation — Sigstore keyless provenance proving it was built from
this repository's CI. There is no long-lived signing key to leak.
```bash
gh attestation verify burnwall-x86_64-unknown-linux-gnu.tar.xz --repo intbot/burnwall
```
- **Checksums.** Each release ships per-file `.sha256` and a combined
`sha256.sum`:
```bash
sha256sum --ignore-missing -c sha256.sum
```
- **Supply-chain hygiene.** The repository runs OpenSSF Scorecard in CI. The
install one-liners are served over HTTPS only; package-manager installs
(Homebrew, `cargo install`, `cargo binstall`) are the recommended trusted
paths, and the npm wrapper publishes with provenance when that channel is
enabled.
- **Open source.** The proxy, scanner, and pricing logic are auditable — the
"no network calls except forwarding" claim below can be checked in the code.

## How Burnwall handles your traffic (TLS & data)

A proxy that terminates or weakens TLS would be a liability. Burnwall does not:

- **TLS is validated, never weakened.** Upstream connections use `rustls`
(`rustls-tls`, with native-TLS disabled) and validate the provider's
certificate like a browser would. Burnwall never disables certificate
validation (no `danger_accept_invalid_certs`) and never injects or installs a
root CA. There is a guard test (`tests/unit/tls_integrity_test.rs`) asserting
these never appear in the source.
- **Responses are read-only.** Burnwall inspects responses to compute cost and
**never modifies them** — your tool receives the provider's bytes unchanged.
- **No plaintext secrets at rest.** API keys pass through in headers and are
never written to disk. Prompt/response **content is never logged** — only
metadata (model, token counts, cost, timestamp).
- **Local only, zero telemetry.** No data leaves your machine except the API
forwarding you configured. No analytics, no phone-home.
- **Fail-open.** If a request body can't be parsed, Burnwall forwards it rather
than break your workflow — it never silently drops your traffic.

## Kill switch

If anything ever misbehaves, `BURNWALL_BYPASS=1` turns the proxy into a pure
relay (no scanning, no budget checks, no storage) for the current session, and
`burnwall self-rollback <version>` reinstalls a prior release.

## Scope

Burnwall reduces risk; it is not a guarantee. Run it as one layer of
defense-in-depth alongside your tool's native permissions/sandbox and least-
privilege credentials — not as a replacement for them.
11 changes: 11 additions & 0 deletions dist-workspace.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,14 @@ tap = "intbot/homebrew-burnwall"
publish-jobs = ["./publish-crates", "./publish-nuget", "./publish-pypi"]
# Run a plan-only check on PRs (don't try to build/publish on every PR).
pr-run-mode = "plan"
# `release.yml` carries a manual patch over the dist-generated workflow (the
# attestation-retry block — re-apply after any `dist generate`). Without this,
# dist's CI-consistency guard fails `plan` because the committed workflow no
# longer matches what dist would emit. Scope is "ci" only, so every other file
# is still checked for drift.
allow-dirty = ["ci"]
# Generate GitHub Artifact Attestations (Sigstore keyless build provenance,
# SLSA Build L2). Every released binary can then be verified with
# `gh attestation verify <file> --repo intbot/burnwall`. No signing key to
# manage — a security tool should be exemplary about its own integrity.
github-attestations = true
6 changes: 5 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,11 @@ The security engine scans the JSON request body before forwarding. It does NOT n
}
```

The scanner does a deep traversal of the JSON looking for string values that match deny patterns. It doesn't need to know which field is which — any string value containing a denied path or command triggers a block.
The scanner does a deep traversal of the JSON looking for string values that match deny patterns. On the LLM proxy path it is **context-aware**: command-shaped rules (denied paths, denied commands, network mounts, destructive commands, exfil techniques) apply only inside tool-call argument subtrees — Anthropic `tool_use.input`, OpenAI `tool_calls` / `function_call` arguments, Gemini `functionCall`. Prose (the system prompt, chat text, tool definitions, tool results) can legitimately *mention* `~/.ssh` or `rm -rf` — project docs describing a deny list, a conversation about backups — and must not be blocked for it. Data-shaped rules (secret detection, DLP) still apply to **every** string leaf, since a credential or card number is worth blocking wherever it sits in the payload.

Within a conversation, command-shaped rules are further scoped to the **latest assistant turn's in-flight tool round** (the trailing assistant message followed only by tool results). Clients resend the full history on every request, so scanning older turns would make one correctly-blocked call re-trigger the 403 forever. The request that carries the dangerous call and its output is blocked — that is the moment the forbidden read's content would leave the machine — but once the user sends a new message the round is adjudicated and the conversation recovers.

MCP `tools/call` bodies keep the strict whole-body semantics: there, the entire payload *is* a tool invocation, so any string value containing a denied path or command triggers a block.

### Pattern Matching Strategy:
- **Path matching:** Expand `~` to actual home dir, normalize paths, check against deny list
Expand Down
Loading
Loading