CodeCouncil reads your coding agent's session transcripts and your repo's git state, and sends review prompts to a model provider you configure. You should know exactly what leaves your machine and what never does. This document is that contract — and because the code is open, every claim here is auditable.
One thing only: review prompts, sent to the model provider(s) you configure (NVIDIA-hosted, OpenRouter, or anything pi supports — your choice, your keys). A prompt can contain:
- excerpts of the coding agent's reasoning and tool calls (truncated, redacted)
- git diffs and capped contents of new/changed files (redacted)
- commit subjects and diffs (redacted)
- your repo's README/CLAUDE.md excerpts and the learned heuristics/knowledge files
If you configure a local model through pi, nothing leaves at all.
Every text field is passed through core/redact.py at observer capture
time — before it is written to disk, before any prompt is built. Credential
shapes (AWS keys, API tokens, private-key blocks, high-entropy assignments to
secret-like names) become «REDACTED:kind» markers. The marker itself is
taught to the critic as a confirmed secret-in-code finding, so secrets are
caught without ever being transmitted. Model-authored text that gets
persisted (issues, rationales, repro commands, distilled facts) is redacted
and capped again at parse time.
Redaction is pattern-based and deliberately precision-first; it is a strong
floor, not a guarantee against every exotic secret format. Review
core/redact.py for the exact patterns.
Two related boundaries:
- Capture never follows a symlink out of the repo.
git ls-fileslists untracked symlinks and a naive read would follow one to, say,~/.aws/credentialsor a sibling checkout — capturing a file that is not part of your project and shipping it in the next prompt.observer/gitwatch.pyresolves each path and refuses anything landing outside the repo root, the same containment the judgment-turn tools already enforce. - Model-authored text is control-character stripped, not just redacted
(
core/redact.py'ssanitize). Findings are printed to your terminal and injected into your coding agent's context, so an ANSI escape sequence in a model-writtenissuestring could otherwise repaint the line above it and misrepresent a finding's severity.
- Your API keys. They live in
~/.codecouncil/env— outside every repository, so they can never be committed regardless of which repo CodeCouncil watches. Keys are read at request time and passed to pi as environment variables; CodeCouncil never persists them elsewhere. - Raw repository history, untouched files, or anything outside the observed diff/transcript window.
- Verification repros run against a copy of the flagged file in a throwaway temp directory — never in your repo.
- Investigation tools on judgment turns are custom, path-jailed
implementations (
critic/pi_extensions/jail.mjs): reads are confined to the repo root (symlink-escape and traversal rejected,.git/.codecouncilexcluded). pi's builtin file tools are deliberately NOT used for this, because they resolve~and absolute paths. - Repros delivered to your coding agent are the verification script
itself, redacted, control-character-stripped, capped, and framed "review
before running". CodeCouncil hands it over as text and never executes it
in your repo — but it is model-authored code, so treat it as a suggestion
to read, not a command to run blind. (Earlier versions of this document
described a
python3/pytestprefix allowlist; that gate applied to the single-shell-command repro format which no longer exists.) - The Claude Code hook (
hooks/peer_hook.py) is fail-open: any internal error exits silently rather than breaking your session.
The Critic's verification (critic/verify.py) and opt-in probe
(critic/probe.py) features ask the model to write a short repro/probe
script, which CodeCouncil then executes on your machine — in a
throwaway staging directory, never in your repo — to prove a finding real
before it's ever delivered.
That execution gets two independent layers (core/sandbox.py):
- A scrubbed environment. The child's environment is a minimal
allowlist built from scratch (
PATH,HOME,LANG/LC_ALL, plusPYTHONPATHpointed at the staging copy), never a copy of the parent's. No API key is in it, andHOMEpoints inside the staging directory. - An OS sandbox. On macOS via
sandbox-exec, on Linux viabwrap: all network egress is denied, and reads under your real home directory are denied (so~/.codecouncil/env,~/.ssh, and your shell history are unreachable). The staging directory stays writable, and the Python interpreter's own prefixes stay readable — necessary because pyenv/asdf install the interpreter inside your home.
Layer 2 is not redundant, and this is worth being precise about because an
earlier version of this document got it wrong. It claimed layer 1 alone
meant "model-authored code cannot read your keys." That was false.
HOME only governs ~ expansion; pwd.getpwuid(os.getuid()).pw_dir
returns your real home regardless, and reading <real home>/.codecouncil/env
by absolute path and POSTing it out was demonstrated working. Environment
scrubbing cannot fix that — getpwuid reads the OS user database, not the
environment — which is why the OS boundary was added.
Scope of the guarantee. The two headline risks (credential theft and
network exfiltration) are closed where a sandbox mechanism exists. It is
still not a full syscall jail: on macOS the profile denies network and home
reads over an (allow default) base, so a script can read world-readable
paths elsewhere on disk — with egress denied, its only channel back is
stdout, which CodeCouncil redacts and caps.
If no sandbox mechanism exists (a Linux host without bwrap), scripts
run with layer 1 only and CodeCouncil prints a warning rather than implying
protection it isn't providing. Set COUNCIL_SANDBOX=require (or
"sandbox": "require" in ~/.codecouncil/config.json) to refuse to execute
instead; off disables sandboxing for debugging.
require degrades, it does not break: verification and probes are skipped
with an explicit "verification skipped" note, and the finding is still
delivered — just without an execution proof attached. It is never recorded as
refuted, because a refusal to test something is not evidence against it.
The default is auto rather than require deliberately: bubblewrap is not
installed by default on most distributions, and defaulting to require would
silently disable the product's core "prove it before speaking" behaviour for
those users. Operators who prefer fail-closed should set it explicitly.
Even so: run CodeCouncil on repositories you would be willing to execute code from. Verification and probe scripts import the file under review, and importing a Python module runs its top-level statements — so "review this repo" does mean "run some of this repo's code", sandboxed.
Open a GitHub security advisory on this repository (Security → Advisories → Report a vulnerability), or open an issue if the report is not sensitive. The redaction layer, the path jail, and the repro-command gate are the highest-value targets — reports there are especially welcome.