1. brindle
  2. Security and trust

Security and trust

For security reviewers weighing brindle Enterprise. What leaves a machine on each plan, how air-gap mode and the signed audit log work, and what policy does when it breaks. Every claim here comes from brindle's own source.

What leaves your machine

Your code goes only to the model providers you pick, under their terms. brindle never sends it to PawDelta. brindle CI is the one exception, because it runs in your GitHub repository; see Privacy.

PlanWhat reaches PawDelta
FreeNothing. No account, no telemetry, no analytics. brindle checks PyPI once a day for a newer release (set BRINDLE_NO_UPDATE_CHECK to stop it), and a native profile that names its model as provider/model fetches the public models.dev catalog once a day.
ProSign-in, a signed license refreshed over the network (sent with the names of the agent CLIs you're signed in to), cloud credential requests if you use federation, settings sync, and hosted learning: a summary of each finished task (kind, size, profile, outcome, tokens, time) under opaque ids. Never task text, prompts, code, or file or repo paths.
TeamEverything in Pro, plus the org policy and the audit feed: delegations, reviews, escalations, merges, worktree removals and spend per model, with the profile, provider and model. Agents, branches and who acted appear only as keyed hashes. Also route-health counts per model.
EnterpriseThe same as Team, plus each repo's owner/name when you use cost centers, or nothing at all in air-gap mode.

The full list, with what we store and for how long, is in Privacy.

Air-gap mode

Enterprise only. Set "airgap": true in .brindle/config.json, or BRINDLE_AIRGAP=1 in the environment. Once a process has loaded an air-gapped repo, no later config turns it off for that process.

  • No requests to PawDelta. Login, license refresh, key fetches, hosted learning, the team policy and the hosted audit feed are all refused before they reach the network. The update check and the models.dev fetch are off too.
  • Local models only. A profile runs only with the native provider on a loopback, private-network or link-local base_url given as an IP address or localhost (for example 127.0.0.1, ::1, 10.x, 172.16-31.x, 192.168.x). Other hostnames count as hosted unless the profile is marked local: true. Hosted providers are refused for workers, reviewers and the chat itself, so brindle will not start unless default_agent is a local profile.
  • An offline license. A signed file, checked against keys pinned in brindle with no network and never refreshed.
  • An offline policy. The org policy comes from .brindle/policy.json. Without it, delegations and merges are refused.
  • Fail safe. Without a license that includes air-gap mode, brindle still blocks everything and says so.

A walkthrough on a machine with no route out:

# 1. Turn it on and point the supervisor at a local model
$ cat .brindle/config.json
{"airgap": true, "default_agent": "developer-local"}

# 2. Install the offline license and the org policy
$ brindle account license install enterprise.license
$ cp org-policy.json .brindle/policy.json

# 3. Check it (an excerpt; names and values are yours)
$ brindle doctor
✓ air-gap          on via "airgap": true in .brindle/config.json: no outbound traffic, local models only
! hosted profiles  codex-heavy: refused in air-gap mode. Only a native profile on a loopback or private-network base_url (or one marked `local: true`) can run; point default_agent, routing and reviewer at one
✓ default agent    developer-local (local: `brindle` can start)
✓ offline license  org org_acme, plan enterprise, features airgap, audit, audit_export, team
✓ offline policy   /srv/payments/.brindle/policy.json
Doctor levels

✓ is fine, ! needs a look, ✗ is a failure. A hosted profile listed with ! is configured but will not run. A hosted default_agent is a ✗.

Signed audit trail

Enterprise records every delegation, review verdict, escalation, merge, worktree removal and spend (cost) event, and every delegation or merge that policy refused, with the reason. The log is local and works in air-gap mode.

  • One append-only JSONL file per repo under ~/.brindle/audit, readable only by your user (0600, in a 0700 directory).
  • Each record carries a sequence number, a UTC time, the event, the SHA-256 of the previous record, its own SHA-256, and an Ed25519 signature over that hash.
  • The signing key is per install, in ~/.brindle/audit/signing.key. brindle audit pubkey prints the public half so you can check exports with your own tools.
  • A head file remembers the last record, so records removed from the end are caught too.
  • brindle audit verify recomputes every hash and signature and names the first record that fails: edited, removed, inserted, reordered or truncated. Verifying and exporting need no license, so a log keeps its value after a plan lapses.
  • Retention prunes old records and leaves one signed checkpoint, so a pruned log still verifies.
$ brindle audit verify
/home/dev/.brindle/audit/payments-3f9c0a1b7d2e4c58.jsonl: 1284 record(s), chain intact, every signature valid

# after a record is edited, the command names it and exits 1
$ brindle audit verify
/home/dev/.brindle/audit/payments-3f9c0a1b7d2e4c58.jsonl: BROKEN at seq 412: record was altered (hash mismatch)

# for your SIEM or a spreadsheet
$ brindle audit export --format jsonl --since 2026-10-01T00:00:00Z > audit.jsonl
$ brindle audit export --format csv > audit.csv
$ brindle audit pubkey

With the audit_export feature, brindle audit ship also sends records to a webhook, Splunk, Datadog or S3 sink you configure. It sends nothing in air-gap mode.

Policy that fails closed

Team and Enterprise set org policy: which providers and models agents may use, which profiles, how many workers run at once, and whether a human must review before a merge. Enterprise adds custom roles, each with its own policy.

  • A policy that errors, gives no answer, or is named in config but cannot be loaded refuses the delegation or merge. It does not skip the check.
  • In air-gap mode, a missing .brindle/policy.json refuses delegations and merges until it is in place.
  • On Enterprise, every refusal is written to the audit log with its reason.
  • brindle enforces policy in the client. For a guarantee no client can bypass, also require reviews with branch protection in your Git host.

Releases and license

  • brindle is published on PyPI as brindle. A GitHub release triggers the build; the release tests must pass and the tag must match the package version. It publishes through PyPI Trusted Publishing, so no long-lived PyPI token exists.
  • The install script, curl -fsSL pawdelta.com/brindle/install | sh, installs uv if missing, installs tmux only through Homebrew on macOS (on Linux it prints the command to run), then installs brindle with uv tool install. BRINDLE_VERSION pins a version. Read the script first if your policy requires it.
  • We do not publish separate checksum files. PyPI lists a SHA-256 digest for every file, and each release carries a Sigstore-signed provenance attestation from PyPI Trusted Publishing.
  • Offline Enterprise licenses are signed, and brindle verifies them against keys built into it.
  • brindle is free to use, on your own machine and at work: Brindle License 1.0 lets you install and run it for any purpose, but not modify it, redistribute it or use it to run a competing service. The source is on GitHub.

Reporting a vulnerability

Email support@pawdelta.com with what you found and how to reproduce it. Please do not open a public issue for a vulnerability.