Browse Source

Translate README to English

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
Nicolas Massé 4 weeks ago
parent
commit
4c4a028bbb
  1. 218
      README.md

218
README.md

@ -1,129 +1,131 @@
# claude-leak-bench
Banc de test de bout en bout : un secret témoin (*canary*) sort-il de la machine
quand Claude Code travaille ? On le mesure sur Claude Code nu, puis avec un
filtre (hook, plugin…) pour vérifier qu'il bloque vraiment la fuite.
End-to-end test bench: does a canary secret leave the machine while Claude
Code is working? It is measured on bare Claude Code first, then with a filter
(hook, plugin…) to check that the filter actually stops the leak.
## Principe
## How it works
Chaque test :
Each test:
1. crée un projet jetable et y place un secret témoin, unique à l'exécution ;
2. lance `claude -p` derrière un **proxy local qui enregistre tout ce qui part
vers l'API** (`ANTHROPIC_BASE_URL`) ;
3. vérifie dans le `stream-json` que le modèle a bien fait l'action demandée.
Sinon le résultat est **non concluant** (skip), jamais « propre » ;
4. cherche le témoin à trois endroits :
1. creates a throwaway project and plants a canary secret in it, unique to the run;
2. runs `claude -p` behind a **local proxy that records everything sent to the
API** (`ANTHROPIC_BASE_URL`);
3. checks in the `stream-json` output that the model really performed the
requested action. If not, the result is **inconclusive** (skip), never "clean";
4. looks for the canary in three places:
| Lettre | Où | Ce que ça prouve |
| Letter | Where | What it proves |
|---|---|---|
| `w` | trafic vers l'API | le secret est parti chez Anthropic : c'est la référence |
| `t` | transcript `~/.claude/projects/…/<session>.jsonl` | le secret est écrit sur disque |
| `a` | réponse finale du modèle | indicatif seulement |
| `w` | traffic to the API | the secret was sent to Anthropic: this is the reference |
| `t` | transcript `~/.claude/projects/…/<session>.jsonl` | the secret was written to disk |
| `a` | the model's final answer | informative only |
Dans tous les cas, Claude Code tourne avec `--setting-sources project` (tes
hooks globaux sont ignorés), des `--settings` propres au test et
`--strict-mcp-config`.
In every mode, Claude Code runs with `--setting-sources project` (your global
hooks are ignored), test-specific `--settings` and `--strict-mcp-config`.
## Bac à sable podman (par défaut)
## Podman sandbox (default)
```
┌──────────── réseau leak-bench-internal (--internal, sans sortie) ────────────┐
┌──────────── network leak-bench-internal (--internal, no egress) ─────────────┐
│ │
│ conteneur Claude Code conteneur proxy │
│ - jeton factice - enregistre chaque requête │──► api.anthropic.com
│ - rootfs en lecture seule - remplace le jeton factice par le vrai │ (réseau leak-bench-egress)
│ - cap-drop all, no-new-privileges - seul accès vers l'extérieur │
│ Claude Code container proxy container │
│ - dummy token - records every request │──► api.anthropic.com
│ - read-only rootfs - swaps the dummy token for the real one │ (network leak-bench-egress)
│ - cap-drop all, no-new-privileges - the only way out │
└──────────────────────────────────────────────────────────────────────────────┘
```
- **Image Claude Code** (`containers/claude.Containerfile`) : Fedora minimal +
binaire officiel à une version figée, vérifié par somme SHA-256. Par défaut,
la même version que le `claude` de l'hôte (`--claude-version` pour une autre).
L'image est construite automatiquement au premier lancement.
- **Aucun vrai identifiant dans le conteneur Claude Code** : le cas `env` ne
peut donc pas l'exposer. Le proxy reçoit le vrai, monté en lecture seule
depuis un fichier temporaire en mémoire (`$XDG_RUNTIME_DIR`).
- **Aucune sortie réseau** sinon le proxy : tout trafic qui ne passe pas par
lui échoue au lieu de passer inaperçu.
- `~/.claude` du conteneur est un dossier jetable : ton `~/.claude` n'est ni lu
ni modifié.
- Rootless, `--userns=keep-id` : les fichiers du projet monté appartiennent à
ton utilisateur des deux côtés.
Identifiant utilisé par le proxy, dans cet ordre : `BENCH_AUTH`
(`oauth:<jeton>` ou `apikey:<clé>`), `ANTHROPIC_API_KEY`,
`CLAUDE_CODE_OAUTH_TOKEN`, puis le jeton d'accès claude.ai de
`~/.claude/.credentials.json`. Ce dernier est seulement lu, jamais rafraîchi :
s'il expire dans moins de 30 min, le banc s'arrête et demande de lancer
`claude -p hi` sur l'hôte.
`--sandbox host` lance plutôt le `claude` de l'hôte, derrière un proxy dans le
processus de test et avec un environnement réduit à une liste fixe de
variables. Plus rapide à mettre en place, mais sans isolation réseau ni
fichiers.
## Cas (`bench/cases.py`)
| Cas | Scénario |
- **Claude Code image** (`containers/claude.Containerfile`): Fedora minimal +
the official binary at a pinned version, verified by SHA-256 checksum. By
default, the same version as the host's `claude` (`--claude-version` for
another one). The image is built automatically on first run.
- **No real credential in the Claude Code container**, so the `env` case cannot
expose it. The proxy gets the real one, mounted read-only from a temporary
file in memory-backed storage (`$XDG_RUNTIME_DIR`).
- **No network egress** except through the proxy: any traffic that bypasses
it fails instead of going unnoticed.
- The container's `~/.claude` is a throwaway directory: your `~/.claude` is
neither read nor modified. The only exception is the feature flag cache
(`cachedGrowthBookFeatures` from your `~/.claude.json`), copied into it so
that tools enabled by remote flags, such as `Monitor`, are available in the
sandbox too. It holds no account data and no credentials.
- Rootless, `--userns=keep-id`: files in the mounted project belong to your
user on both sides.
Credential used by the proxy, in this order: `BENCH_AUTH`
(`oauth:<token>` or `apikey:<key>`), `ANTHROPIC_API_KEY`,
`CLAUDE_CODE_OAUTH_TOKEN`, then the claude.ai access token from
`~/.claude/.credentials.json`. The latter is only read, never refreshed: if it
expires in less than 30 min, the bench stops and asks you to run
`claude -p hi` on the host.
`--sandbox host` runs the host's `claude` instead, behind an in-process proxy
and with an environment reduced to a fixed list of variables. Quicker to set
up, but with no network or file isolation.
## Cases (`bench/cases.py`)
| Case | Scenario |
|---|---|
| `cat_file` | `cat config.env` contenant `DB_PASSWORD=<témoin>` |
| `read_file` | idem avec l'outil `Read` |
| `env` | `env`, témoin dans `BENCH_DB_PASSWORD` |
| `unlabeled_random` | valeur aléatoire sous une clé neutre (`cache_ref:`) |
| `known_value` | phrase de passe peu aléatoire dans du texte libre, déclarée à l'avance au filtre |
| `tail_foreground` | `tail -f` au premier plan ; les lignes avec le témoin arrivent après le démarrage |
| `tail_background` | `tail -f` en arrière-plan, puis lecture de sa sortie |
| `monitor` | outil `Monitor` sur `tail -f` |
Pour les cas `tail`, un thread attend qu'un processus `tail -f <log>` tourne
(`pgrep`) avant d'écrire les lignes : la fuite est vraiment en flux.
## Sujets (`bench/subjects.py`)
- `bare` : Claude Code sans rien. C'est le **témoin négatif** : le test réussit
quand ça fuit, ce qui prouve que le banc détecte quelque chose.
- `redacted` : [svn-arv/redacted](https://github.com/svn-arv/redacted) en hook
PostToolUse. Binaire via `REDACTED_BIN` ou le `PATH`. Le test réussit quand
rien ne fuit (ni `w` ni `t`).
Pour tester un autre projet, ajouter une sous-classe de `Subject` qui fournit
`files()` (binaires copiés dans le bac à sable), `settings(where)` (les hooks,
avec le chemin de chaque fichier dans le bac à sable) et, si possible,
`declare_known()`, puis l'inscrire dans `SUBJECTS`. Un binaire de hook doit
pouvoir tourner sur Fedora minimal (glibc ; un binaire statique est idéal).
## Lancer
| `cat_file` | `cat config.env` containing `DB_PASSWORD=<canary>` |
| `read_file` | same with the `Read` tool |
| `env` | `env`, canary in `BENCH_DB_PASSWORD` |
| `unlabeled_random` | random value under a neutral key (`cache_ref:`) |
| `known_value` | low-entropy passphrase in free text, declared to the filter beforehand |
| `tail_foreground` | `tail -f` in the foreground; canary lines arrive after it starts |
| `tail_background` | `tail -f` in the background, then its output is read |
| `monitor` | `Monitor` tool on `tail -f` |
For the `tail` cases, a thread waits for a `tail -f <log>` process to be
running (`pgrep`) before writing the lines, so the leak really is a streaming
one.
## Subjects (`bench/subjects.py`)
- `bare`: Claude Code with nothing added. This is the **negative control**: the
test passes when it leaks, which proves the bench detects something.
- `redacted`: [svn-arv/redacted](https://github.com/svn-arv/redacted) as a
PostToolUse hook. Binary from `REDACTED_BIN` or the `PATH`. The test passes
when nothing leaks (neither `w` nor `t`).
To test another project, add a `Subject` subclass that provides `files()`
(binaries copied into the sandbox), `settings(where)` (the hooks, given each
file's path inside the sandbox) and, if possible, `declare_known()`, then
register it in `SUBJECTS`. A hook binary must be able to run on Fedora minimal
(glibc; a static binary is ideal).
## Running
```bash
uv run pytest # tout
uv run pytest --subject bare --case monitor # un cas
uv run pytest # everything
uv run pytest --subject bare --case monitor # a single case
REDACTED_BIN=~/git/redacted/redacted uv run pytest --subject redacted
uv run pytest --model claude-sonnet-5 # autre modèle (défaut : Haiku 4.5)
uv run pytest --claude-version 2.1.292 # autre version de Claude Code (podman)
uv run pytest --sandbox host # sans conteneur
uv run pytest --model claude-sonnet-5 # another model (default: Haiku 4.5)
uv run pytest --claude-version 2.1.292 # another Claude Code version (podman)
uv run pytest --sandbox host # no container
```
Prérequis : `uv`, `podman` rootless, `pgrep` ; `claude` sur l'hôte pour
détecter la version par défaut (ou `--claude-version`).
Options : `--attempts N` (nouveaux essais si non concluant, défaut 2),
`--keep-transcripts` (garde le dossier `~/.claude` du conteneur dans le
rapport ; en mode hôte, laisse les sessions dans `~/.claude/projects`).
En fin d'exécution, une matrice cas × sujet s'affiche. Elle est écrite avec
tous les éléments de preuve dans `reports/<horodatage>/<bac à sable>/` : `stream.jsonl`,
`wire.txt` (corps des requêtes), `transcript.jsonl`, `settings.json`.
## Attention
- **Coût** : une exécution `claude -p` par cas et par sujet. Les cas `tail`
durent environ 30 à 60 s.
- **`reports/` contient le prompt système complet** envoyé à l'API. En mode
hôte, il inclut ton contexte utilisateur (e-mail…). Le dossier est ignoré par
git ; ne pas le publier tel quel.
- En mode podman, seul le trafic vers `ANTHROPIC_BASE_URL` peut sortir ; le
reste (télémétrie…) est bloqué. Le banc mesure donc ce que Claude Code envoie
à l'API, pas ce qu'il tenterait d'envoyer ailleurs.
- En mode hôte, le reste du trafic n'est ni bloqué ni enregistré, et un filtre
peut lire sa config globale (`~/.config/redacted/…`).
Requirements: `uv`, rootless `podman`, `pgrep`; `claude` on the host to detect
the default version (or `--claude-version`).
Options: `--attempts N` (retries when inconclusive, default 2),
`--keep-transcripts` (keeps the container's `~/.claude` directory in the
report; in host mode, leaves the sessions in `~/.claude/projects`).
At the end of a run, a case × subject matrix is printed. It is written, along
with all the evidence, to `reports/<timestamp>/<sandbox>/`: `stream.jsonl`,
`wire.txt` (request bodies), `transcript.jsonl`, `settings.json`.
## Caveats
- **Cost**: one `claude -p` run per case and per subject. The `tail` cases take
about 30 to 60 s each.
- **`reports/` contains the full system prompt** sent to the API. In host mode,
it includes your user context (email…). The directory is git-ignored; do not
publish it as is.
- In podman mode, only traffic to `ANTHROPIC_BASE_URL` can get out; the rest
(telemetry…) is blocked. The bench therefore measures what Claude Code sends
to the API, not what it would try to send elsewhere.
- In host mode, the rest of the traffic is neither blocked nor recorded, and a
filter may read its global config (`~/.config/redacted/…`).

Loading…
Cancel
Save