SOURCE (laptop) SINK (Mac mini / cloud VM)
+---------------------------+ +-----------------------------+
| Chrome stable | | Chrome stable |
| Cookies SQLite | | Cookies SQLite |
| Safe Storage (Keychain)| | Safe Storage (Keychain) |
| | | |
| agentcookie source | | agentcookie sink (launchd) |
| - read SQLite (RO) | | - listen :9999/sync |
| - decrypt w/ local key | AES-GCM | - decrypt seal |
| - filter by blocklist | over HTTP | - check proto + seq |
| - wrap in envelope | ================> | - filter by blocklist |
| - seal w/ peer key | on tailnet | - write Chrome SQLite |
| | (WireGuard) | - write sealed sidecar |
+---------------------------+ +-----------------------------+
^ ^
| |
+----------- Tailscale tailnet ------------------+
| (WireGuard, ACLs) |
+--------------------------------------------------+
| Package | Purpose |
|---|---|
cmd/agentcookie |
CLI entry point (cobra). |
internal/cli |
Subcommand implementations: source, sink, pair, status, version. |
internal/chrome |
Read + decrypt Chrome cookies on macOS via Keychain Safe Storage + SQLite. Schema-aware INSERT for the write path. |
internal/transport |
AES-GCM seal/open with key = SHA-256(secret). |
internal/config |
YAML loaders for source.yaml, sink.yaml, blocklist.yaml. Tilde expansion, defaults, validation. |
internal/pairing |
X25519 + HKDF handshake. Source listens for pairing; sink connects with the printed code. Both sides derive identical 32-byte keys. |
internal/keystore |
Per-peer key files at ~/.config/agentcookie/keys/<peer>.json mode 0600. |
internal/protocol |
SyncEnvelope (versioned), SequenceTracker (in-memory replay defense), BlocklistMatcher (SQLite-LIKE patterns, case-insensitive). |
internal/cdp |
Tiny Chrome DevTools Protocol client: Probe + Dial + Call. One method we care about: Storage.setCookies. |
agentcookie source --onceruns on the laptop.- Reads
~/.config/agentcookie/source.yamlfor sink URL andpeer.hostname. - Reads
~/.config/agentcookie/blocklist.yamlfor opt-out domain patterns. - Loads the paired key for
peer.hostnamefrom~/.config/agentcookie/keys/. - Calls
security find-generic-passwordto get Chrome Safe Storage; derives the per-machine AES key. - Opens Chrome's Cookies SQLite read-only with
immutable=1. Drops rows matching each blocklist pattern. - Decrypts each
encrypted_value(v10 prefix, AES-128-CBC, IV = 16 spaces, PKCS#7). - Wraps the cookies in a
SyncEnvelopewith version, hostname, monotonic Sequence. - AES-GCM-seals the envelope with the paired key.
- POSTs to the sink's
/syncURL.
On the sink, in the /sync handler:
- Reads the raw bytes.
- Loads the paired key for the configured source hostname; AES-GCM-opens the payload. Wrong key -> 401.
- JSON-unmarshals the
SyncEnvelope. - Checks
ProtocolVersion == 1. Mismatch -> 400. - Checks
Sequenceagainst the in-memorySequenceTracker. Replay -> 409. - Filters cookies against the sink's own
blocklist.yaml. Dropped hosts are counted for logging. - If
cdp.enabled, probeshttp://<host>:<port>/json/version, dials the browser-level WebSocket, sendsStorage.setCookies. On any failure, falls back to step 8. - Otherwise opens the sink's Cookies SQLite read-write, re-encrypts each value with the SINK's Chrome Safe Storage key, upserts rows via a schema-aware INSERT ... ON CONFLICT (handles Chrome's
top_frame_site_key,source_type,has_cross_site_ancestorcolumns dynamically).
- Source:
agentcookie pair --as sourcegenerates an X25519 ephemeral keypair and a fresh base32 code (e.g.YILU-OIVK). Listens on:9998/pair. Prints the code and the sink-run command. - Sink:
agentcookie pair --as sink --peer <source-host> --pair-url ... --code YILU-OIVKgenerates its own X25519 keypair, POSTs(code, sink_pub, sink_hostname)to source. - Source checks the code (constant-time compare). Computes
shared = X25519(source_priv, sink_pub). Deriveskey = HKDF-SHA256(shared, salt=code, info="agentcookie-pair-v1")[:32]. Replies with(source_pub, source_hostname, fingerprint). - Sink computes the same
shared, derives the same key. Verifies the source's fingerprint matches its own. Writes the key to~/.config/agentcookie/keys/<source-host>.jsonmode 0600. - Source's listener shuts down; the key it derived is also written to disk on the source side, keyed by the sink's hostname.
| Boundary | Enforced by |
|---|---|
| OS user separation on source and sink | macOS user accounts, file mode 0600 on key files and configs |
| Cookie value at rest | Chrome Safe Storage per-machine AES key + Keychain access prompt |
| Cookie value in transit | AES-GCM with paired key + Tailscale WireGuard channel |
| Pairing authenticity | Pairing code mixed into HKDF salt; MITM derives different key |
| Sink-side opt-out | blocklist.yaml on sink; cookies for blocklisted hosts are dropped before writing |
| Replay defense | SequenceTracker in sink memory; rejects equal-or-lower Sequence |
| Protocol stability | ProtocolVersion int in every envelope; breaking changes bump the number |
See threat-model.md for what each of these protects against and what they don't.