This document captures the wire format between source and sink so future clients (a Linux sink, a Chrome extension on the sink, a hosted relay) can interoperate.
- Transport. HTTP over a Tailscale tailnet. POST
/syncon the sink machine carries the sealed payload as the request body withContent-Type: application/octet-stream. - Authenticated encryption. AES-256-GCM. The key is derived from the source-sink pair via X25519 + HKDF-SHA256 (see
internal/pairing), or from the legacysecurity.shared_secretYAML field. Each message carries a fresh 12-byte nonce as the first 12 bytes of the ciphertext; the GCM tag is appended automatically. Wrong-secret payloads are rejected by the AEAD tag check. - Envelope. Inside the seal, the plaintext is a JSON
SyncEnvelope.
{
"protocol_version": 1,
"source_hostname": "my-laptop.tailnet.ts.net",
"sequence": 1747432817,
"cookies": [ { ... }, { ... } ]
}
Field-by-field:
protocol_version(int, required). Sinks reject envelopes whose value does not equal the sink's compiled-in version. Bumping the version is a breaking change.source_hostname(string, required). The source's announced hostname. The sink uses this as the key in its replay-defense bookkeeping. The same hostname appears in the paired key file under~/.config/agentcookie/keys/.sequence(int64, required). Monotonically increasing per source. Sinks reject an envelope whosesequenceis less than or equal to the highest sequence seen for that source since process start. The source usestime.Now().Unix()by default; any monotonically increasing source is acceptable.cookies(array, required). Each cookie matches thechrome.CookieGo struct:host_key,name,value(plaintext after source-side decrypt),path,expires_utc,is_secure,is_httponly,last_access_utc,has_expires,is_persistent,priority,samesite,source_scheme,source_port. Values are integers and strings, never raw bytes.
- Decrypt the body with the configured shared/paired key. Reject
401 Unauthorizedon failure. - JSON-unmarshal the envelope. Reject
400 Bad Requeston failure. - Check
protocol_version == 1. Reject400with a clear message otherwise. - Check
sequenceagainst the in-memory tracker forsource_hostname. Reject409 Conflictif not strictly greater than the last seen value. - Filter
cookiesagainst the sink's local cookie policy inblocklist.yaml. In blocklist mode, matching hosts are dropped. In allowlist mode, only matching hosts are kept. The sink logs dropped host counts and reports them to the source via the response body. - Write the remaining cookies. CDP path first if
cdp.enabledand Chrome reachable; otherwise SQLite path.
The sequence tracker is in-memory only. Restarting the sink clears it; a captured payload could be replayed once after a restart. Durable replay defense is a v0.2 follow-up; the envelope is shaped so it can absorb a timestamp window or a server-issued nonce without breaking the version.
The sink reads ~/.config/agentcookie/blocklist.yaml independently of the source. If the source pushes a cookie for a host the sink policy rejects, the sink drops it. This means the sink owner has the final say on what state may land in their Chrome, even if the paired source is fully compromised. The source-side policy still runs first as a bandwidth and privacy optimization (no point sealing cookies that will be dropped).
Default blocklist schema:
version: 1
domains:
- pattern: "instacart.com"
description: Instacart exact host
- pattern: "%.instacart.com"
description: Instacart subdomainsOmitted policy means blocklist, preserving the v0.3 sync-all default when
the file is missing or domains is empty. policy: blocklist is accepted and
behaves the same way.
Explicit allowlist schema:
version: 1
policy: allowlist
domains:
- pattern: "github.com"
description: GitHub exact host
- pattern: "%.github.com"
description: GitHub subdomainsAllowlist mode is for high-trust/headless agent deployments where only named
sessions should leave the source machine. Only matching host_key patterns
sync; all other cookie hosts are dropped on the source and again on the sink.
An empty allowlist syncs no cookie hosts and is reported by agentcookie doctor.
Patterns use SQLite LIKE semantics (% wildcard). Matching is case-insensitive and matches against Chrome's host_key column, which includes a leading dot for subdomain cookies. Prefer agentcookie accounts off <domain> for normal site toggles; it writes the exact host plus a subdomain-safe %. pattern.
A missing blocklist.yaml still means sync-all; a present but unparseable policy intentionally halts sync instead of falling back to sync-all.
- v1 is the current wire format. Source and sink must both speak it.
- Future versions: bump
protocol_versionand update both source and sink. Sinks may carry compatibility shims for one prior version during transition windows. - Out-of-band fields (e.g. filter sync, signed bundles, cookie diffs) will arrive as additional optional envelope fields under v1 first, then potentially graduate to required in v2.