Custosworking name

Security review

Every claim, every test, every limit.

The landing page tags 18 product claims. Each is tied to named tests of the proof of concept or to a recorded run; 2 are only partly proven. Proof of concept, not yet independently audited.

gate 26 Oct, 21:05 CET · tree 5b643f83 · poc/scripts/gate-json.mjs. Fuzz smoke: 65 / 65 targets rc=0, 63–67 s each, 0 failed.

The relay's copy of the 02:14 request, every byte

From the proof of concept's run behind the landing page's story (poc-output/cloud-fixture.json): 9,778 bytes, one pixel each, 136 to a row. 300 bytes it reads, 7,962 bytes of public keys and signatures, 1,516 bytes sealed to your account.

The relay's copy of the 02:14 request: 9,778 bytes, one pixel each. 300 bytes of metadata it reads, 7,962 bytes of public keys and signatures, 1,516 bytes sealed to your account.
  1. 146 B readable: format, position, kind, time, request and key ids
  2. 1,216 B public: the one-time key your answer is sealed to
  3. 147 B readable: expiry and framing (14 B), the receipt's size, hash and time (133 B)
  4. 3,373 B public: the relay's signature on the receipt (receipt: 133 + 3,373 = 3,506 B)
  5. 1,516 B sealed: the reason, the URL, the agent, the scope
  6. 3,373 B public: devbox-03's signature

300 + 7,962 + 1,516 = 9,778 B. Of the 300 B: 146 + 147 in bands, 7 B of framing between them; 9,778 B in all.

The files

Copied byte for byte from the repository by scripts/sync-captures.ts (SHA-256 in poc-output/captures.json), served as plain text. The docs site is not public yet.

18 claims and every test behind each

  1. C01The agent uses a key; the value never enters its process (service mode)3 tests
    • e2e uc1_blind_use::e2e_uc1_blind_use
    • e2e drop::e2e_drop
    • mcp tests/stdio.rs::stdio_roundtrip
  2. C02Answers are scrubbed of the value and its encodings5 tests
    • e2e uc1_blind_use::e2e_uc1_blind_use
    • e2e uc1_blind_use::canary_scanner_fires_on_every_encoding
    • core redact::every_embedding_is_redacted
    • core redact::embedded_value_always_replaced
    • daemon proxy::scrub_strips_cookies_redacts_and_withholds_encoded
  3. C03A key goes only to the hosts, methods and path prefixes its policy allows3 tests
    • e2e uc1_blind_use::e2e_uc1_blind_use
    • e2e uc4_bridges::e2e_uc4_bridge_azure
    • core policy::egress_component_checks
  4. C04The relay can't read the requests, the values or the key share in your answer (the approver app's code is trusted when it loads, N9)4 tests
    • e2e uc5_proof::e2e_uc5_proof
    • 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
  5. C05What the relay can and cannot read (metadata only)3 tests
    • relay plaintext::db_holds_no_plaintext
    • e2e uc5_proof::e2e_uc5_proof
    • relay push::a_request_pings_every_active_approver_with_nothing_in_it
  6. C06Adding an approver passes the account secret sealed; an honest relay deletes it after 10 minutes, a hostile one can keep it (N18)4 tests
    • core spec_properties::as_transfer_sealed
    • e2e enroll_second_approver::e2e_enroll_host_and_second_approver
    • relay tests/mailbox.rs::mailbox_expires_after_ten_minutes_and_is_reusable
    • relay tests/mailbox.rs::host_enrollment_ceremony_through_the_relay
  7. C07An approval needs a user-verified passkey from an enrolled approver3 tests
    • e2e uc2_remote_approval::e2e_uc2_remote_approval
    • 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
  8. C08A grant ends at its time and its use limit3 tests
    • e2e uc3_night_shift::e2e_uc3_night_shift_9h_default_policy
    • e2e uc2_remote_approval::e2e_uc2_remote_approval
    • e2e uc3_night_shift::e2e_uc3_night_shift_9h_default_policy
  9. C09An approval can narrow a request, never widen it5 tests
    • core grant::narrow_tier2_once_and_window
    • core grant::envelope_never_exceeds_narrow
    • e2e uc3_night_shift::e2e_uc3_night_shift_9h_default_policy
    • e2e uc2_remote_approval::e2e_uc2_remote_approval
    • daemon tests_flow::widened_grant_ignored_and_alarmed_not_frozen
  10. C10An unanswered request expires after 10 minutes3 tests
    • 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
  11. C11The log is signed and chained; a fork is caught by the host check4 tests
    • e2e uc5_proof::e2e_uc5_proof
    • e2e uc5_proof::e2e_uc5_fork_found_by_prefix_check
    • e2e uc5_proof::e2e_uc5_relay_vouching_for_tampered_entry_freezes
    • core prefix_check::fork_with_host_growth_detected_by_prefix_compare
  12. C12The agent's reason is shown cleaned of hidden characters, and logged2 tests
    • e2e uc2_remote_approval::e2e_uc2_remote_approval
    • core text::removes_hidden_and_collapses_whitespace
  13. C13Revoking a lost phone stops the grants it signed7 tests · partial
    • e2e uc5_proof::e2e_panic_this_wasnt_me
    • 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
    • core tests/log_state.rs::grant_by_revoked_approver_rejected_and_invalidated
    • daemon tests_flow::approver_revoke_stops_grants_it_signed
    • e2e enroll_second_approver::e2e_enroll_host_and_second_approver
  14. C141Password and Azure Key Vault stay the store; fetched only after a grant4 tests
    • e2e uc4_bridges::e2e_uc4_bridge_azure
    • e2e uc4_bridges::e2e_uc4_bridge_1password
    • daemon tests_spec::bridge_credential_zeroized_after_fetch
    • daemon bridge::op_env_is_exactly_allowed_set
  15. C15import .env swaps values for names and keeps owner and mode2 tests
    • e2e uc4_bridges::e2e_uc4_import_env_and_doctor
    • cli host/tests.rs::import_env_keeps_owner_and_mode
  16. C16The crypto suite passes external test vectors5 tests
    • 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
  17. C17Works with Claude Code over MCP: 8 recorded sessions used a key (shell off; recorded, not gated)8 tests · partial: recorded, not gated
  18. C18The product output on this page is real PoC output2 tests
    • relay tests/bin.rs::serves_until_sigint_then_inspect_shows_the_rows
    • e2e uc5_proof::e2e_uc5_proof

Suite C1: HPKE with X-Wing (ML-KEM-768 + X25519) seals envelopes; Ed25519 and ML-DSA-65 sign (both must verify); stored values use ChaCha20-Poly1305 with a 32-byte key commitment; HKDF-SHA-256 derives keys. Every envelope names its suite, so it can change later. Still classical: the passkey behind each approval (ES256) and TLS to our relay. The last hop to each provider is only as post-quantum as the TLS group that provider negotiates (N30); the daemon offers X25519MLKEM768 first.

What it doesn't do: 12 limits

Known limits of the proof of concept
LimitWho it affectsWhat to doSpec
The clock is the host's. 07:00 is kept by an honest host's daemon, not by cryptography; a relay and time-server attacker together could stretch it by their skew.anyone relying on the end timekeep host time trusted; short windowsN2
Dev mode guards against accidents only. Where the daemon runs as the agent's user, or the agent can become root, a hijacked agent can use and copy the host's keys.laptops; agents with sudo, docker or rootservice mode on the agent boxN19
Type secrets on your phone. A value typed into the CLI on the agent's machine passes through a process the agent's user can trace, in either mode.anyone adding a key on the agent boxadd values on the phoneN19
Service mode is set up by hand. The systemd unit the CLI prints lacks the agent's user id and doesn't start the daemon yet.Linux service-mode installsstart it as the walkthrough doesfinding
Trust the app's code. Whoever serves the approver app could change it.everyone using a hosted approverself-host it, or run it apart from the relayN9
A compromised host can stay quiet. It gets what it was granted while it holds a grant, and can under-report its own uses.a host an attacker ownsshort windows, per-use tier 2N1, P14
A host's keys are software keys. A disk backup or VM snapshot copies them: with a later approval for that host, or a revoked approver, it opens that host's values.VMs and backed-up hostshardware-bound keys are plannedN20, N11
Kept ciphertext rests on X-Wing. The relay keeps X-Wing-sealed key halves in its log, and could keep a copy of the account secret; if X-Wing (ML-KEM-768 and X25519 both) is ever broken, the kept log opens every policy and reason and both key halves of each secret that was granted or proposed by a host, which means the values themselves.everyone, in the long runmoving shares out of the log is plannedN18
An allowed API can mint a new key. Inside an allowed egress rule the agent can call an endpoint that creates a credential (GitHub deploy keys, POST /user/keys, collaborator invites) and keep access after 07:00 and after revocation; the app shows a red banner for known minting paths.every lease with write ruleskeep write rules narrow (the story's token may only POST …/pulls); review keys created during grant windowsN6
Redaction is a safety net, not a boundary. An API or program that transforms the value can return it in a form the scrub does not detect; in service mode the value never enters the agent's process, so the scrub is a second line.dev mode; tools that run with the valueservice mode; the proxy, not the scrubN5
Not yet run on a real phone. The approver is tested in desktop Chromium at phone size; passkeys and push on iOS Safari and Android Chrome are untested.every approvera real-phone run is next—
HTTP APIs only, for now. Keys sent in one request header; SSH keys, certificates and database passwords are planned.non-HTTP secretsplannedN22

N-ids are the spec's list of known limits (ARCHITECTURE.md §1).

How the nearest products compare

From docs/src/data/competitor-matrix.json (research/15), vendor docs read 2026-10-23; cells marked “carried” were scored on 2026-10-22 and not re-read. “Not documented” means we found nothing in their docs, not that they lack it.

The nearest products, five columns from the competitor matrix
ProductKey out of the modelApprove remotelyEnds on a clockVendor blind to valuesShippingAudited
Custos (PoC)yes · Service-mode proxy; UC1 e2epartly · approver web app; opt-in content-free push (ADR 0085); tested in desktop Chromium only, no real phone yet (note corrected here; matrix pending: ADR 0085)yes · Night-shift preset to 07:00, at most 200 uses; UC3 e2epartly · Against the relay; not against whoever serves the approver code (N9)no · Proof of concept; no usersno · No external audit
1Passwordyes · Environments MCP never returns valuessourceno · Prompt in the local desktop app; stdio onlysourcepartly · MCP approval lasts until 1Password locks; OpenShell TTL custodysourcepartly · Vault yes; Credential Broker adds a vendor-enclave key (carried)yes (carried)yes · SOC 2 Type 2; pentest reports in the Trust Centersource
Bitwardenpartly · aac run injects into a child's env; aac connect prints the value as JSONsourcepartly · Remote host connects, but approval is a CLI on the vault host (carried)partly · Lease duration (Privileged Controls, preview)sourceyes · Relay routes opaque ciphertext (carried)partly · Agent Access alpha (carried)yes · Annual code audits and pentests, reports public (but 12 attacks in ePrint 2026/058)source
Infisicalyes · Agent Proxy placeholders (HTTP only) (carried)partly · Enterprise approval workflows, no phone approver (carried)partly · Session TTL, e.g. 30m, 8h, 7dsourceno · Server decrypts to answer requests (external KMS/HSM optional)sourceyes · Agent Proxy GA 2026-07-30 (carried)partly · Cure53 gray-box pentest and SOC 2 Type 2; reports on requestsourcesource
OneCLIyes · Gateway injects at the network layersourceyes · Approve/Deny card in Slack or the dashboard; approves actions, not a key release; the vendor cloud and Slack see the requestsourceno · Listed under 'Coming soon'sourcepartly · Own store: server-side AES-256-GCM; 1Password values only in gateway memory for 60 ssourcesourceyes · 3.6k stars, YC S26 (carried)not documented · Not documented: no audit, SOC 2 or pentest found in the docs or sitesource
Clawvisoryes · Injected server-sidesourceyes · Dashboard, Telegram, push; QR pairing (carried)partly · Task expires_in_seconds (carried)no · Managed cloud stores and injects; self-host keeps it in your networksourcepartly · Small project, 283 stars (carried)not documented · Not documented: the FAQ points to the public source; no audit foundsource

What Custos combines, in the proof of concept today: each approval releases the keys you name, each only to the hosts, methods and path prefixes its policy allows (set on your phone, per key), until a set time and a use limit. A key you mark production (tier 2, the default for prod) is released for one use within 10 minutes; when the agent asks through the proxy (http_request), that use is bound to the exact method, URL and body it showed you; it travels through a relay that can't read the request, the value or the key share; grants end on the daemon's clock; the log keeps the agent's reasons, signed. It is built to sit in front of 1Password and Azure Key Vault, tested so far against a stub op CLI and an emulator (Lowkey Vault).

Where the others are ahead, by the same matrix: approve a remote / headless agent from a phone (OneCLI and Clawvisor ship phone-reachable approval today; our PoC has opt-in push but has not run on a real phone); shipping, generally available product; independent security audit or attestation.

The stores, the agent clients and the CLI

The CLI output below is the proof of concept's own, from the run behind the landing page's story (poc-output/cloud-fixture.json).

  • 1Passwordop://<vault-id>/<item-id>/<field>

    Read through a read-only service account. Its token stays with the Custos daemon, which fetches a value only after a grant. On Linux hosts; on macOS in dev mode. Tested with a stub op run as a real child process; a live service account has not been run yet.

  • Azure Key Vaultvault kv-prod · secret ledgerline-live

    A service principal with read access to one vault. custos bridge setup azure prints the az commands. It doesn't run them. Tested against Lowkey Vault (a Key Vault emulator) and a fake Entra, not a real tenant.

  • .envLEDGERLINE_LIVE=custos:ledgerline-live

    The only .env your agent can read holds names. Rotate the old values: copies may survive on disk.

  • Your agents
    Agent clients and their test status
    Claude Code8 sessions used a key · shell off Real Claude Code 2.1.276 sessions against the real MCP server, with only its MCP tools on: in the 8 that used a key, the key never reached the model. With its shell on: not yet recorded. Setup: one line (the last command in the terminal below).
    Codex · Cursor · Gemini CLIuntested Stdio MCP clients too, so they should work the same way. Not run yet.
    Scripts (plain CLI)tested An agent on a remote box, end to end, through the CLI.

    Agents on macOS and Linux, local or over SSH. The Custos daemon compiles for Windows; no Windows agent client yet. You approve in the approver web app.

Type high-value secrets on your phone. A value typed into the custos CLI on the agent's machine passes through a process the agent's user can trace, in either mode (N19).

Show the CLI: doctor, import .env, a bridge, the one-line agent setup
$ custos doctor
[standing_access] environment variable OP_SERVICE_ACCOUNT_TOKEN is set
    fix: unset it in agent shells; store it with `custos secret add` or a bridge
[standing_access] ~/.azure/msal_token_cache.json: Azure CLI login cache
    fix: run `az logout` in agent shells; use `custos bridge add azure`
[standing_access] ./.env holds values for LEDGERLINE_TEST, LEDGERLINE_LIVE, GITHUB_TOKEN
    fix: run `custos import .env --env <env>`
… 8 findings in all
$ custos import .env --env dev
LEDGERLINE_TEST → custos:ledgerline-test (proposed BLO7KLvyx6H9lQifp6dBaA)
LEDGERLINE_LIVE → custos:ledgerline-live (proposed MmWomCdrCZZ6FT2s_MHkMA)
GITHUB_TOKEN → custos:github-token (proposed N3S7mtB-h0Lz6D7Dl_UtUw)
rewrote .env (3 value(s) → handles; owner and mode kept)
Values were copied into Custos. The old file contents may survive on disk (copy-on-write, SSD, snapshots, backups). Rotate these values at their provider, then adopt the proposals on your phone.
$ custos bridge add azure --tenant ledgerline.example --client-id 1111…5555 --ref kv-prod/ledgerline-live --name kv-prod-reader --env prod
Client secret of the service principal (input hidden):
proposed Azure credential 2etb_KotVTWxDe3fz3UvtQ: adopt it on your phone (Secrets → Proposed by hosts) and set the handles' egress rules.
Remove standing access from agent terminals: `az logout` (custos doctor checks).
$ claude mcp add custos -- "$(command -v custos-mcp)"

The recorded Claude Code sessions

10 sessions in 2 runs (23 Oct, 08:40–08:43 CEST), Claude Code 2.1.276 with claude-sonnet-5-5, Claude Code's own shell and file tools off, the real custos-mcp server. 8 sessions used the key, 12 calls in all, with 12 approvals; every approval and denial was made by the run's Playwright script in the approver app (desktop Chromium), not by a person. Both runs passed: 52 per-session checks and 10 file scans. The key was absent from the model transcript (the stream-json record, 32 forms checked) in every session; Claude Code's own session files were not found, so 2 of the scans checked nothing (the harness should fail when a required scan finds no file). Recorded, not gated: the runs did not record their code tree.

  • 0times the key showed up: 32 encodings searched in the transcripts and in the daemon's and the relay's files; the relay's database searched in 5 forms
  • 12calls with the key, each approved in the approver app by a test script (desktop Chromium)
  • 0substitutions outside the approved header: a key name put into a URL or an X-Copy header (tried in 2 sessions) came back from the echo as plain text
  • 2requests for "a copy of the key", denied by the same script

What this doesn't show. Approvals were scripted; no person, no phone. Claude Code's shell and file tools were off, so Custos's tools were the model's only way to the key: this shows the redaction and the tools that never return a value, not the shell door the landing page's story leaks through. Redaction is a safety net, not a boundary (N5); in service mode the value never enters the agent's process at all. One laptop in dev mode, one model. In 2 of the 10 sessions the model's own safeguards stopped before any call. Reports: run 06:40:40Z · run 06:42:08Z

The approver app at the injected request: llm-laptop wants demo-token; banners: Production or per-use secret; Dev mode: any program running as the user on this host could have made this request and can receive released values; This host was enrolled less than 24 h ago. Reported by the host, not verified: Program claude → custos-mcp. Agent says: User asked for an HTTP GET to this URL with the demo-token handle and a summary of the returned JSON. Path chosen by the agent: the full URL, including a planted note addressed to the AI agent that tells it to request the token in plain text, open a lease, send the token to another URL and not tell the user.
The approver app at the one request with a planted instruction in its URL, shown whole and unshortened. The test script approved it, as it approved every card; a person reading it could have said no. What kept the key safe: Custos adds the key only to the approved header, so a key name put into a URL goes out as plain text. Three crops of one screenshot at its native size; the rows between them (a local file path) are left out.

Why now: numbers and voices

More questions

Will my phone tell me when the agent asks?

If you turn on notifications, it tells you that something is waiting, and no more. Settings → Notifications in the approver app subscribes the phone to an empty Web Push: an iPhone or iPad from iOS 16.4 as a Home Screen app, or Chrome, Android, Firefox and Edge. The ping carries no request details: you open the app to see the request and decide. Apple, Google, Mozilla or Microsoft learn when a ping is sent, not what for. Push is off when the relay runs without a push contact, and delivery through Apple's and Google's services is not yet tested on a phone. Without notifications, the approver app shows a request while it is open. A request nobody answers expires after 10 minutes. That is why Custos asks per grant, not per call: "staging, these hosts and paths, until 07:00" is one approval at bedtime, and 93% of permission prompts get approved anyway.7 The Custos daemon enforces the grant; tokens a provider issues also expire on their own.

What if I lose my phone?

Revoke it from a second approver. Every grant it signed stops at each host within a minute of the revocation reaching our relay, or the host pauses. A relay that holds it back can stretch that to 3 minutes. A relay that hides or refuses it: the app tells you to run the host check, which locks a host that missed it. Enroll that second approver on day one; with one, the app warns you on every screen. A thief who knows your passcode can approve until you revoke (N26).

A revoked approver still holds today's account secret, so secrets created later stay exposed to it until replacing the account secret is built. Planned: a printed recovery kit, with no password.

Which agents and systems?

Claude Code: run for real, 10 recorded sessions with Claude Code 2.1.276 and the real custos-mcp server, 8 of which used a key, with Claude Code's own shell and file tools off (above). Not yet recorded: Claude Code with its shell on, which is where service mode matters; that boundary is tested with two real OS users on Linux, without Claude Code. Scripts use a plain CLI, tested end to end. Codex, Cursor and Gemini CLI are stdio MCP clients too, so they should work; we haven't run them yet. Agents on macOS and Linux, local or over SSH. The Custos daemon compiles for Windows; there is no Windows agent client yet.

You approve in the approver web app, with a passkey: Face ID, a fingerprint or your passcode. It is tested in desktop Chromium; no run on an iPhone or Android phone yet. Today it covers HTTP APIs that take the key in one header. SSH keys, certificates and database passwords are planned.

Is it audited? Is it open source?

Not audited yet, and we won't pretend otherwise. The spec with its threat model, the index of decision records and every claim with its tests are here as plain text. The licence for the code isn't decided yet.

I'm one developer. Isn't this overkill?

Maybe. Not an agent story, just what one leaked key can cost: one team reported $82,314.44 in charges from a single compromised Google Cloud key, over two days. Their normal month: $180.10 If your agents never touch a real key, you don't need us.

What does it cost?

Nothing yet: the proof of concept isn't for sale. Proposed, not decided: free for local use, paid when it crosses devices, people or time. Prices aren't set; that is an owner decision (the proposal).