From 4c4a028bbb927a6d8f7c179bfb5302e027276266 Mon Sep 17 00:00:00 2001 From: Nicolas MASSE Date: Sun, 11 Oct 2026 12:05:21 +0200 Subject: [PATCH] Translate README to English Co-Authored-By: Claude Opus 5.5 --- README.md | 218 +++++++++++++++++++++++++++--------------------------- 1 file changed, 110 insertions(+), 108 deletions(-) diff --git a/README.md b/README.md index d1044a4..789731d 100644 --- a/README.md +++ b/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/…/.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/…/.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:` ou `apikey:`), `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:` or `apikey:`), `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=` | -| `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 ` 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=` | +| `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 ` 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///` : `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///`: `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/…`).