# Claims registry Every sentence on the landing page that states a **design property** of the product ("can't read", "never enters", "expires", "is enforced by"…) is tagged in the HTML with `data-claim="Cnn"` and listed here with the Rust test of the proof of concept that proves it. A claim whose test is `missing` must not ship to production until the test exists and passes. Since critique iteration 2 (final review) the full claims list (every cited test per claim), the limits table, the competitor comparison and the Claude Code phone card are on the security review page next to the landing page, `/evidence/` (`src/pages/evidence/index.astro`); "on the page" below means either page. `tests/unit/claims.test.ts` enforces: every tag on the page is registered here, every row here is on the page, every row has a status, claim-style wording never appears outside a tagged element, and the wording ARCHITECTURE §14.5 forbids ("wakes you", a recovery kit not marked planned, Windows agents, `{{placeholder}}`, `run_with_secrets`, `bridge scan`, "XChaCha", "verbatim") is absent. `tests/unit/claims-citations.test.ts` resolves every backticked citation of the form scope, two colons, fn (optionally [markers]) below to a real `fn` (and comment marker) in a Rust file under `poc/` (ADR 0068 naming rule). `tests/unit/parity.test.ts` checks the page's PoC output against the fixture the Rust run produced (C18). **Every Rust test cited below was run on 2026-10-23**: in the PoC's final gate on tree `33d8870d` (`poc/scripts/gate-json.mjs`, measured 09:47 CEST; `cargo llvm-cov --workspace --all-targets`: 690 passed, 0 failed, 20 613 / 20 613 lines; every recorded step on that tree; `poc-output/gate.json`), the same tree the showcase capture ran on (`poc/demo/showcase/manifest.json` `source_tree_at_start/end`; `pnpm fixture` also ran on it until critique iteration 3, which re-ran it on a later tree to move the story to `.example` hosts: `poc-output/cloud-fixture.json` `pocTree` names that tree, and the page footer and `pnpm release-check` say the two differ); earlier the same day, before review round 5, one by one (the evidence section of the page reads each row's status and first test from this file at build time, `src/data/evidence.ts`), with `CARGO_TARGET_DIR=poc/target/landing-claims cargo test -p `: 42 test names, each matched exactly 1 test, 42 passed, 0 failed. Round 5 (spec review, SPEC-CONFORMANCE-C5-1/-2/-5) added 6 citations to C06, C09 and C13; each was run by name on 2026-10-23 and passed, and each fails under the mutation of its property listed in `tests/unit/claims-citations.test.ts`. Round 6 (SPEC-CONFORMANCE-C6-2, ADR 0085) added three relay `tests/push.rs` tests to C17 and one to C05; `cargo test -p custos-relay --test push` ran on 2026-10-23: 9 passed, 0 failed. Crates: `e2e` = `custos-e2e` (`poc/tests-e2e/tests/`, ADR 0062: real relay over TCP + SQLite, real host daemons on Unix sockets, scripted approver on `custos-core`, agents via MCP, CLI and raw JSON-RPC, canary rule §14.3 after every flow); `core`, `daemon`, `relay`, `cli`, `mcp` = `custos-core`, `custos-daemon`, `custos-relay`, `custos-cli`, `custos-mcp`. Non-Rust evidence (the approver PWA's own tests) is named where it adds something, never instead of a Rust test. IDs run C01–C18 with no gap. Until 2026-10-23 the last row was numbered C19 although no C18 was ever assigned (an early numbering slip, not a withdrawn claim); it was renumbered to C18 after critique round 5. The docs' spec (`docs/src/content/docs/appendix/spec.md`, the C19 row of its change table) still uses the old number. | ID | Claim as phrased on the page | Where | Rust tests that prove it | Status | | --- | --- | --- | --- | --- | | C01 | The agent names a key; the Custos daemon, as its own OS user, adds the value to the outgoing HTTPS request; the value never enters the agent's process or output | hero sub (with the service-mode qualifier under it), guarded act H2/caption, the service mode / dev mode table (`#modes`, its "Tested" cell reads the Linux run from `poc-output/gate.json`), list_secrets, Ledgerline's view, evidence table, comparison row, FAQ 1 | e2e `uc1_blind_use::e2e_uc1_blind_use` (MCP `list_secrets`/`http_request`/`audit_recent` and `custos http`: 0 canary hits in 12 encodings over every agent-visible message), e2e `drop::e2e_drop`, mcp `tests/stdio.rs::stdio_roundtrip`. The service-mode boundary with two real OS users: `daemon/tests/real_perms.rs` (feature `real-perms`, Linux, as root), run by `sh scripts/linux-perms.sh` (the PoC's own `poc/scripts/coverage-linux.sh`, `COV_PACKAGES="-p custos-daemon"`, rust:1.94.0 in Docker, source mounted read-only) on 2026-10-23 from 09:13 CEST (the final gate's Linux step), daemon tree `c3f3d0fe` (hash of the daemon and its path dependencies, the same before and after the run, and the daemon as it is): pass 1 (non-root) 221 tests passed, pass 2 (root) real_perms 11 / 11 passed, merged daemon line coverage 100 % (7 797 / 7 797). The final gate's service-mode walkthrough (`poc/demo/service-mode/transcript.txt`, served unchanged at `/evidence/service-mode-transcript.txt`; gate.json step `service-mode`: 22 ok, 24 refused, 0 failed, two real uids, `--network none`) is shown under "Where that holds" with three marked crops of whole lines, and its one finding (the unit `custos service install --print` prints has no `--client-uid`, so it does not start the daemon) is a stated limit until the PoC fixes it. `src/data/gate.ts` compares that hash with the daemon sources at build time and a release build refuses a mismatch. FAQ 1 and the bridges terminal state the N19 limits (dev mode; values typed into the CLI on the agent's machine are traceable by the agent's user in either mode) | exists | | C02 | Answers are scrubbed as a safety net: exact value, base64, hex, URL and JSON forms, any run of ≥ 12 characters, replaced by `«custos:NAME»` | guarded act caption, model view, FAQ 1 | e2e `uc1_blind_use::e2e_uc1_blind_use`, e2e `uc1_blind_use::canary_scanner_fires_on_every_encoding` (positive control), core `redact::every_embedding_is_redacted`, core `redact::embedded_value_always_replaced` (proptest), daemon `proxy::scrub_strips_cookies_redacts_and_withholds_encoded`; the page's model view is the redactor's real output (parity test) | exists | | C03 | An approval releases the keys it names; each key goes only to the host, method and path its egress rules allow (host, method and path prefix, set per key in its policy, not in the approval; a once grant for a tier-2 key is bound to one exact request); otherwise the request is refused and nothing is sent. Critique iteration 3 replaced "one key to one host, method and path", which the 22:46 grant (two keys, two hosts) contradicted | hero sub, 22:42 turn, FAQ 2 and 4, security review moat line, the N6 limit (an allowed API that mints keys) | e2e `uc1_blind_use::e2e_uc1_blind_use` (evil.example, `http://`, IP literal, localhost, `_method`, encoded separators → `egress_denied`, nothing sent), e2e `uc4_bridges::e2e_uc4_bridge_azure` (refused before Entra/Key Vault), core `policy::egress_component_checks`; the 22:44 refusal on the page is real output with 0 upstream requests (parity test) | exists | | C04 | Our relay carries the question and your answer; it can't read the request details, the value or the key share inside a grant (it does read that a grant or denial exists, when, and each grant's window and use limit: C05); neither half opens anything alone; qualifier: the approver app's code is trusted when it loads | relay H2 and caption (inside the night act), "can't read" list (request details; the key share, sealed to the request's one-time key; the value, stored only encrypted under a key it never holds), key path figure (HTML: AS unlocks a_i; a_i sealed to the request's one-time key; h_i ⊕ a_i = K → DEK → the value, ARCHITECTURE §3, §7.5.4, §8 step 6), evidence table, comparison row, FAQ 4 and 5 (FAQ 5 states the HNDL residual N18, ADR 0034) | e2e `uc5_proof::e2e_uc5_proof` (`World::assert_no_leaks` over the relay SQLite file, its log and `custos-relay inspect`: 0 hits), core `spec_properties::stolen_relay`, core `spec_properties::revoked_approver_with_relay_cannot_decrypt`, core `tessera::host_share_alone_fails_and_approver_share_alone_fails` | exists | | C05 | The relay reads ids, kinds, sizes, times, device labels, each device's platform and isolation (service mode or not), how often each key id is used and by which host, whether you approved or denied and when, whether a denial was "This wasn't me", each grant's window and use limit, and, with notifications on, which push service each approver uses; that push service learns when a request is waiting, never what (the push is empty, ADR 0085); it can't read names, reasons, URLs or the requested scope (ARCHITECTURE P1, N10) | cloud act (byte picture and its band labels, the answer paragraph in three branches, lists). The approve and no-answer branches use the fixture's grant row; the deny branch states the spec's `Denial = {request_hash, code}` with code 1 / 2 / 3 = denied / not-me / number-mismatch (ARCHITECTURE §5.4), which the fixture did not run | relay `plaintext::db_holds_no_plaintext`, e2e `uc5_proof::e2e_uc5_proof` (inspect dump: names, reasons, hosts, paths absent), relay `push::a_request_pings_every_active_approver_with_nothing_in_it` (the push body is empty; no request data in any header); the page's byte counts and grant window are computed from the real relay rows (parity test: no name, reason, URL or path in them) | exists | | C06 | The account secret lives on your approver devices; adding one passes it through our relay sealed to the new device; an honest relay deletes it after 10 minutes, and a copy a hostile relay keeps stays sealed with X-Wing (N18, ADR 0034 amendment) | key path figure + caption | core `spec_properties::as_transfer_sealed`, e2e `enroll_second_approver::e2e_enroll_host_and_second_approver` (approver-to-approver transfer over the real relay mailbox; canary rule over `AS`), relay `tests/mailbox.rs::mailbox_expires_after_ten_minutes_and_is_reusable` (a frame is gone at 10 min), relay `tests/mailbox.rs::host_enrollment_ceremony_through_the_relay` (after a read returns the `Added` frame the mailbox lives at most 60 s more, ADR 0058 §3) | exists | | C07 | Nothing on the agent's machine (or the relay) can approve; every approval needs a user-verified passkey (Face ID, fingerprint or passcode) from an approver you enrolled; attestation is not checked yet, so FAQ 2 states N32 | phone (Verified zone, with the request fingerprint the agent also prints: ADR 0077), 02:14 hint, key path figure, ledger approve row ("you, on your phone (verified)"), evidence table | e2e `uc2_remote_approval::e2e_uc2_remote_approval` (a grant without a valid WebAuthn UV assertion → relay 422), core `tests/log_state.rs::grant_rules [grant_by_host_rejected, grant_without_uv_rejected]`, e2e `uc5_proof::e2e_uc5_fork_found_by_prefix_check` | exists | | C08 | Grants are scoped and time-boxed, enforced by the Custos daemon; inside the grant the agent works without asking; the grant ends at 07:00; the cut-off uses the host's clock, so a relay and time-server attacker together can stretch it by their skew (N2, stated in FAQ 5) | hero sub, night caption, FAQ 3, 4 and 5 | e2e `uc3_night_shift::e2e_uc3_night_shift_9h_default_policy` (lease until 07:00 over a 9 h accelerated night, ≤ 200 uses, no use after 07:00), e2e `uc2_remote_approval::e2e_uc2_remote_approval` (unused grant dead after 10 min); the page's night (uses, 07:00:30 `needs_approval`) is the fixture run | exists | | C09 | An approval can narrow a request, never widen it; the picker offers only grants inside the envelope (tier 2: once, ≤ 10 min) | 22:46 night-shift card, phone decision chips, FAQ 2 | core `grant::narrow_tier2_once_and_window`, core `grant::envelope_never_exceeds_narrow` (proptest), e2e `uc3_night_shift::e2e_uc3_night_shift_9h_default_policy` (10 000 asked → 200), e2e `uc2_remote_approval::e2e_uc2_remote_approval` (10 uses asked on tier 2 → 1), daemon `tests_flow::widened_grant_ignored_and_alarmed_not_frozen` (a grant wider than the request reaches the host: ignored, request shown denied, alarm, no freeze; the e2e rows narrow inside the scripted approver, so this is the test that fails if the host stops narrowing); fixture run: 500 asked → 200 (parity test) | exists | | C10 | A request nobody answers expires after 10 minutes; the ledger row is read from the request's own expiry | phone "expires in", 02:24 branch, ledger | core `spec_properties::request_expiry_bound`, daemon `tests_spec::eph_key_wiped_after_expiry`, e2e `uc3_night_shift::e2e_uc3_night_shift_9h_default_policy` (three requests expire unanswered); the relay row on the page has `expires − time = 600 000 ms` (parity test) | exists | | C11 | Every entry is signed by the device that caused it and hash-chained to the one before; `custos audit verify` checks both; ledger rows marked derived (expiry, end of window) are read from an entry's own times, not entries; a relay showing devices different histories is caught by the 4 check words typed into `custosd verify`, which locks the host; a compromised host can under-report its own uses | ledger caption + foot, FAQ 5 | e2e `uc5_proof::e2e_uc5_proof` (`custos audit verify` on both hosts), e2e `uc5_proof::e2e_uc5_fork_found_by_prefix_check` (FORK and lock), e2e `uc5_proof::e2e_uc5_relay_vouching_for_tampered_entry_freezes`, core `prefix_check::fork_with_host_growth_detected_by_prefix_compare` | exists | | C12 | The agent's reason is shown in its own words, cleaned of hidden characters, and kept in the signed log | phone (Agent says), ledger caption, FAQ 2 and 4 | e2e `uc2_remote_approval::e2e_uc2_remote_approval` (hidden character removed and counted), core `text::removes_hidden_and_collapses_whitespace`; PWA `request-view.test.ts` (badge) | exists | | C13 | Revoking a lost phone stops every grant it signed at each host within a minute of reaching the relay, or the host pauses (P9, 60 s freshness); a relay that holds it back can stretch that to 3 minutes (§6.4: 120 s + 60 s); a relay that hides or refuses it: the host check locks the host; a revoked approver keeps today's account secret (N11) | FAQ 6 | e2e `uc5_proof::e2e_panic_this_wasnt_me` (device revoke + revoke.epoch stop the host), core `log_state::device_revoke_stops_host`, daemon `tests_flow::revocation_epoch_and_device_revoke_stop_use`, core `spec_properties::revoked_approver_plus_host_seed_reads_post_revocation_secret` (asserts the N11 limit). Revoking an *approver*: core `tests/log_state.rs::grant_by_revoked_approver_rejected_and_invalidated`, daemon `tests_flow::approver_revoke_stops_grants_it_signed` (a second approver revokes the phone that signed an active lease: grant and cached value wiped, next use `needs_approval`, nothing sent), e2e `enroll_second_approver::e2e_enroll_host_and_second_approver` (the phone revokes the tablet over the real relay: the tablet's grant, which worked one call before, now gives `needs_approval`). Open: "within a minute" rests on the daemon's sync interval, not on a timed e2e assertion | partial | | C14 | 1Password and Azure Key Vault stay the store; the daemon fetches a value only after a grant; 1Password on Linux hosts, on macOS in dev mode; the page says each bridge was tested against a stand-in ("tested against a stub op CLI; no live account yet", "tested against an emulator (Lowkey Vault) and a fake Entra") and that Custos "is built to sit in front of" them | bridges (tiles, caption, fold), the review page's moat line | e2e `uc4_bridges::e2e_uc4_bridge_azure` (fake Entra + Key Vault, exact URLs, cached per grant, dev-mode host refused), e2e `uc4_bridges::e2e_uc4_bridge_1password` (stub `op` as a real child; macOS dev mode allowed, service mode refused), daemon `tests_spec::bridge_credential_zeroized_after_fetch`, daemon `bridge::op_env_is_exactly_allowed_set`. The bridges terminal on the page is real CLI output of the fixture run. Not run: a live 1Password service account (`make e2e-1password-live`, needs the owner's token) | exists | | C15 | `custos import .env` replaces values with handles, keeps owner and mode, and tells you to rotate | 22:45 turn, bridges | e2e `uc4_bridges::e2e_uc4_import_env_and_doctor`, cli `host/tests.rs::import_env_keeps_owner_and_mode`; the page's .env and import output are the fixture run (parity test) | exists | | C16 | Suite C1: HPKE with X-Wing (ML-KEM-768 + X25519) seals envelopes; Ed25519 + ML-DSA-65 (both must verify); stored values use ChaCha20-Poly1305 with a 32-byte key commitment (AEAD-C1; HPKE's own AEAD is not committing, ADR 0005); HKDF-SHA-256; every envelope names its suite; still classical: the WebAuthn factor (ES256, N27), TLS to the relay (N10), provider TLS on the last hop | security review page (`/evidence/`), under the claims list | core `kem::xwing_draft11_vectors`, core `hpke::hpke_pq_vectors_for_c1`, core `sig::composite_draft_vectors`, core `aead::wycheproof_chacha20poly1305`, core `kdf::wycheproof_hkdf_sha256` (every ADR 0039 vector set passes: C1 is not provisional, `crates/core/README.md`) | exists | | C17 | The real `custos-mcp` binary works with Claude Code over MCP: since critique iteration 3 the landing page shows one evidence tile, "12/12 Claude Code calls got the key back redacted" with its limits attached (shell off, approvals scripted, dev mode, recorded, not gated; redaction is a safety net, not a boundary, N5), and the security review's Claude Code section says "10 sessions in 2 runs … Claude Code's own shell and file tools off … 8 sessions used the key, 12 calls in all, with 12 approvals; every approval and denial was made by the run's Playwright script" (12 approve decisions in the reports' `phone[]`, all made by the run's Playwright script, never a person: the section's limits box says "Approvals were scripted"), the client table "8 sessions used a key · shell off" and FAQ 7; since critique iteration 2 neither the hero eyebrow nor the meta description cite these runs and states what those sessions do not show: Claude Code's own shell and file tools were off, one dev-mode laptop, one model, 2 sessions stopped by the model's safeguards before any call, no code tree recorded for the runs, so the evidence is recorded, not gated (like C13, this row is partial). The page does not claim Claude Code's own session files were searched: the reports' `~/.claude/projects` scan read 0 files in both runs (`captures.json` `sessionFilesScanned`; a finding for the PoC owner in research/14) (every number is counted from `poc/demo/llm-session//report.json` by `landing/scripts/sync-captures.ts`; `tests/unit/round4.test.ts` recounts them). Codex, Cursor and Gemini CLI are stdio MCP clients that should work and are labelled untested; agents on macOS and Linux, local or over SSH; the daemon compiles for Windows, no Windows agent client; approval in the approver web app (Chromium tested, no phone device run); opt-in, content-free Web Push (ADR 0085) on iOS ≥ 16.4 as a Home Screen app, Chrome, Android, Firefox and Edge says only that a request is waiting: the app must be opened to see and decide, push is off when the relay runs without `--push-subject`, and real APNs/FCM delivery is not yet tested on a phone (N15). The "wakes you" ban stays: a ping does not show or decide anything | hero eyebrow, meta description, Claude Code section (`#claude-code`), bridges client table, evidence list, comparison row, phone note under the night act, waitlist list, FAQ 3 (phone) and 7 | demo `llm-session/2026-10-23T06-40-40Z-71466b/report.json` and demo `llm-session/2026-10-23T06-42-08Z-a76c70/report.json` (the recorded Claude Code runs, under `poc/demo/`: outside the gate and the tree hash), mcp `tests/stdio.rs::stdio_roundtrip` (real binary), e2e `uc2_remote_approval::e2e_uc2_remote_approval` (agent on the remote devbox through the CLI), e2e `uc1_blind_use::e2e_uc1_blind_use` (MCP). Windows: `make check-windows` compiles `custos-daemon` only (STATUS.md §5). No Claude Code binary is started by any Rust test: the stdio test sends the JSON-RPC messages an MCP client sends to the real binary, against a fake daemon socket. Non-Rust, real client: `poc/scripts/llm-session.sh` (`make llm-proof`, `tests-e2e/llm/proof.mjs`) ran Claude Code 2.1.276 (claude-sonnet-5-5, `--tools ""`, only the five MCP tools allowed) twice on 2026-10-23, 08:40–08:43 CEST, 5 prompts each (benign, terminal number, direct and indirect injection, exfiltration attempts) against the release `custos-mcp`, daemon and relay and a private build of the approver PWA driven by Playwright: 10 / 10 sessions passed, 62 checks, 0 failed (canary absent in 32 encodings from the transcripts; absent from the 23 daemon and relay files of each run; `relay inspect --search` 0 hits in 5 forms with a positive control; the `~/.claude/projects` scan found 0 files, so it shows nothing). Push: relay `push::a_request_pings_every_active_approver_with_nothing_in_it` (mock RFC 8030 push service: every active approver pinged, VAPID verified per RFC 8292 §4.2, empty body, nothing in any header), relay `push::subscription_rules_and_relay_info` (only active approvers subscribe; push off without `--push-subject`), relay `push::revoked_devices_and_gone_subscriptions_are_removed`; non-Rust: `approver/tests/e2e/push.spec.ts` (Chromium: worker under the real CSP, CDP push → generic notification). Open: a recorded session with Claude Code's shell on (service mode against a hijacked agent is proven by the two-uid Linux run, not by Claude Code); a manual phone push run (STATUS.md) | partial | | C18 | The relay panel, the byte picture and the Custos output below the rewind (tool results, relay rows, CLI output, the phone's fields) are real output of the PoC (devbox-03's service mode in that run is simulated in one process, ADR 0062, and the page says so in the session header, under "Where that holds" and in the footer: the OS-user boundary is shown by the separate two-uid Linux run, C01); the hero's terminal and phone screen are one recorded run (`poc/demo/showcase`, its manifest SHA-256 printed under it; lines only left out, each marked), and the Claude Code section's phone card is three marked crops of a recorded session's screenshot | hero plate, cloud caption and figure, Claude Code section, footer | relay `tests/bin.rs::serves_until_sigint_then_inspect_shows_the_rows` (the real `inspect` binary), e2e `uc5_proof::e2e_uc5_proof`; `landing/scripts/gen-cloud-fixture.ts` runs `scripts/fixture-rs` (the e2e harness's relay, daemon, MCP server, CLI and approver, ending with the canary rule) and the real `custos-relay inspect`; `tests/unit/parity.test.ts` checks every printed value against the inspect lines; the hero: `poc/scripts/capture-showcase.mjs` (one run, `--verify` re-hashes every file), copied by `landing/scripts/sync-captures.ts` only after each file's SHA-256 matches the manifest, PNG → lossless WebP (`cwebp -lossless -exact`, crops with `-crop`, recorded in `poc-output/captures.json`); `tests/unit/round4.test.ts` checks every shown terminal line against `overnight/terminal.txt` and the manifest hash against `poc/demo/README.md` | exists | Also on the page but not a product property (format only, checked by `parity.test.ts`): the handle `custos:name`, the marker `«custos:name»`, MCP tool names `list_secrets`, `request_access`, `http_request` (all in the fixture run), CLI verbs `doctor`, `import .env`, `bridge add azure`, `bridge setup`, `audit verify` (fixture run). The approver phone on the page is recreated from the PWA's request screen (`poc/approver/src/ui/screens/request.ts`) and its shell (`shell.ts`: the one-approver banner), and says so on the page. It has the same zones and rows as the real screen (Program, Executable and Folder with "reported by the client process", Session with the PWA's `SESSION_CAVEAT`, Host time zone), filled with the fixture run's verified words, request words (= the `fingerprint` the agent read at 02:14, printed on the night log; parity test), reason, path, executable, folder and session. No field is substituted (critique round 3): the Program row shows the run's real lineage, "claude → landing-fixture", marked † with a note that the run hosted the MCP server inside its own program (`scripts/fixture-rs`), where an install runs the `custos-mcp` binary. The proof-of-concept note ("shown only while this app is open") sits outside the phone, not inside the app.