# Troubleshooting

> Common failures and recovery: pairing `connection refused`, sink Keychain prompts, missing `~/go/bin` on the sink, stale Chrome `SingletonLock`, DBSC-suspect drops, and the FAQ.

- Repository: mvanhorn/agentcookie
- GitHub: https://github.com/mvanhorn/agentcookie
- Human docs: https://grok-wiki.com/public/docs/mvanhorn-agentcookie-137da38edfae
- Complete Markdown: https://grok-wiki.com/public/docs/mvanhorn-agentcookie-137da38edfae/llms-full.txt

## Source Files

- `docs/faq.md`
- `skill/SKILL.md`
- `internal/cli/doctor.go`
- `docs/runbook-v0.10-keychain-access.md`
- `internal/cli/wizard_verify_adapters.go`

---

---
title: "Troubleshooting"
description: "Common failures and recovery: pairing `connection refused`, sink Keychain prompts, missing `~/go/bin` on the sink, stale Chrome `SingletonLock`, DBSC-suspect drops, and the FAQ."
---

`agentcookie doctor` is the primary triage surface: it walks fifteen check categories (binary signature, Tailscale, config, keystore, sink listener, sink/source state, sealing, adapter coverage, CDP injector, secrets bus, secret coverage, binary install, DBSC, cookie delivery) and prints one line per check with a `Remediation:` hanging below every FAIL or WARN. Exit code is `1` if any check FAILs, `0` otherwise. Most of the failure modes below are first surfaced by doctor; the resolution paths come from the install skill, the v0.10 keychain runbook, and the dry-run incident logs that drove them.

## Run doctor first

```bash
agentcookie doctor                     # human output
agentcookie doctor --json              # stable JSON envelope (DoctorReport)
ssh <sink-hostname> 'agentcookie doctor'
```

The JSON envelope is shaped for agent consumption:

```json
{
  "version": "v0.14.x",
  "exit_code": 1,
  "checks": [
    {"name": "Sink listener", "severity": "fail",
     "detail": "nothing bound on 100.80.229.80:9999",
     "remediation": "launchctl bootstrap gui/$UID ~/Library/LaunchAgents/dev.agentcookie.sink.plist"}
  ]
}
```

`Severity` is one of `ok`, `warn`, `fail`, `info`, `skipped`. The `Adapter coverage`, `Secret coverage`, `Binary install`, `DBSC`, and `Cookie delivery` checks are deliberately WARN-only — they describe recoverable misconfigurations, not blockers.

<Tip>
After every `wizard install` over SSH, run `ssh <sink> 'agentcookie doctor'` and `agentcookie status --json` on both sides. The combination of (port bound) + (recent state file) is what proves the LaunchAgent is actually the listener, not a competing process.
</Tip>

## Pairing fails with `connection refused`

Symptoms: the sink wizard exits at the pairing handshake step, the source's push loop logs `connect: connection refused`, or `nothing bound on …:9999` appears in doctor's `Sink listener` row.

There are three distinct ports/paths to keep separate:

| Port  | Role   | Owner                | Purpose                                      |
| ----- | ------ | -------------------- | -------------------------------------------- |
| 9998  | source | `agentcookie wizard install --as source` | one-shot pairing listener (X25519 + HKDF)    |
| 9999  | sink   | `agentcookie sink` daemon              | long-running `/sync` listener (tailnet-only) |

Resolution order:

<Steps>
<Step title="Confirm tailnet reachability">
On the source, run `tailscale status` and confirm the sink hostname is online. The sink's listener binds to its tailnet IP (e.g. `100.80.229.80:9999`); a `127.0.0.1` listen address indicates a botched install — re-run `agentcookie wizard install --as sink` so listen-address validation rejects the fallback.
</Step>
<Step title="Confirm the listener is actually bound">
On the sink: `lsof -nP -iTCP:9999 -sTCP:LISTEN`. If empty, `launchctl list | grep dev.agentcookie` to see whether the LaunchAgent is loaded. Bootstrap it with `launchctl bootstrap gui/$UID ~/Library/LaunchAgents/dev.agentcookie.sink.plist`.
</Step>
<Step title="Check the sink log for startup-time aborts">
`tail -50 ~/.agentcookie/logs/sink.err.log`. A historical failure mode: the sink process runs and processes adapters but never binds the listener because a Keychain read failed under SSH/LaunchAgent context. Doctor's `Sink listener` check is the operational truth — if it FAILs while the process exists, the daemon silently skipped binding.
</Step>
<Step title="Check Tailscale ACLs">
If the sink listener is bound but the source still hits `connection refused`, your tailnet ACLs may be blocking traffic on the sync port. Confirm `tailscale ping <sink-hostname>` works from the source.
</Step>
</Steps>

## Sink Keychain prompts on first cookie read

Symptoms: a `kooky`-using CLI on the sink (`instacart-pp-cli`, `yt-dlp`, `gallery-dl`) hangs on first read, or a GUI **Keychain Access wants to use the login keychain** dialog fires on the sink's desktop. Doctor's `Cookie delivery` row reports `partial: real Default profile written but the Chrome Safe Storage key has not been granted to cookie readers`.

The wizard's auto-grant runs three strategies in order; if all fail, install does **not** abort. It prints a loud warning and points at the runbook.

| Strategy                          | Notes                                                                                  |
| --------------------------------- | -------------------------------------------------------------------------------------- |
| `delete-and-recreate-with-A`      | Primary. Best-effort delete of the Chrome Safe Storage item, then `security add-generic-password -A` with a fresh random password. No login-password prompt when the item is freshly created. |
| `partition-list:apple-tool,apple` | Fallback. On macOS 15+ typically requires the login keychain password and fails from a LaunchAgent context. |
| `trust-list:<binary>`             | Per-binary fallback via `--extra-binary`. Adds one binary to the existing item's trust list. |

The supported recovery paths:

<Tabs>
<Tab title="One-password SSH grant (preferred)">

The v0.13 path: one login-password entry over SSH, no GUI click.

```bash
ssh <sink> 'agentcookie wizard set-keychain-access'
# non-interactive form for automation:
ssh <sink> 'AGENTCOOKIE_LOGIN_PASSWORD=… agentcookie wizard set-keychain-access'
```

This collapses any duplicate Chrome Safe Storage items value-preserved, then re-sets the partition list so the signed daemon (`teamid:NM8VT393AR`) and the `apple-tool:` partition (yt-dlp, gallery-dl) both read. Verify with:

```bash
ssh <sink> 'agentcookie internal keychain-probe'   # prints "ok len=N", N>0
ssh <sink> 'agentcookie doctor'                    # Cookie delivery: universal
```
</Tab>
<Tab title="One-time GUI fallback">

Use when `set-keychain-access` reports `FAILED` (typically when the item already exists with a restrictive ACL from a prior install). At the sink's desktop (Screen Sharing is fine):

1. Open **Keychain Access.app**.
2. Search for `Chrome Safe Storage`.
3. Double-click the row, open the **Access Control** tab.
4. Select **Allow all applications to access this item**.
5. Click **Save Changes**, type the login keychain password once.

Then re-run `agentcookie wizard set-keychain-access` over SSH; the `delete-and-recreate-with-A` strategy now succeeds because the GUI click cleared the prior ACL restrictions.
</Tab>
<Tab title="Stay degraded">

Pass `--skip-keychain-access` to the wizard. Universal cookie delivery is off; only agentcookie-aware tools (sidecar/adapter readers) see synced cookies. `kooky`-using CLIs prompt for Always-Allow on first run, per binary.
</Tab>
</Tabs>

<Warning>
Doctor surfaces a related WARN: `race: N Chrome Safe Storage keychain items exist`. The install-time Chrome-relaunch race can leave duplicate items, and a later reader may hit the un-granted duplicate. `set-keychain-access` quiesces Chrome and converges the duplicates — this is the only fix; manually deleting an item risks losing the encryption key.
</Warning>

## `agentcookie: command not found` on the sink after `go install`

The sink's `$PATH` lacks `~/go/bin`. SSH non-interactive shells do not source `~/.zshrc` by default, so `go install` puts the binary in a directory the SSH session cannot see on the next invocation.

Three fixes, ordered by durability:

```bash
# 1. Invoke by absolute path (always works, no shell config change)
ssh <sink> '~/go/bin/agentcookie wizard install --as sink ...'

# 2. Source the shell rc explicitly for the SSH command
ssh <sink> 'source ~/.zshrc && agentcookie wizard install --as sink ...'

# 3. Add ~/go/bin to the sink's login PATH (durable)
ssh <sink> "echo 'export PATH=\$HOME/go/bin:\$PATH' >> ~/.zprofile"
```

<Note>
A subtler variant: two diverging `agentcookie` binaries on the same machine — for example `~/go/bin/agentcookie` (stale, on PATH) and `~/bin/agentcookie` (the one the LaunchAgent actually runs). Doctor's `Binary install` check WARNs when the binaries differ in size or mtime, listing every location. The fix is to reinstall so every location is the same build, or delete the stale copy.
</Note>

## Sink Chrome subprocess crashes (`SingletonLock`)

Symptoms: the sink writes appear to succeed but no Chrome subprocess survives, `~/.agentcookie/logs/sink.err.log` shows repeated startup failures, or the wizard hangs at `Chrome did not publish DevToolsActivePort`.

The managed Chrome subprocess shares `~/.agentcookie/chrome-profile/` as its `--user-data-dir`. If a prior Chrome process crashed without cleaning up, the stale `SingletonLock` file blocks the supervisor from spawning the next instance.

```bash
ssh <sink> 'rm ~/.agentcookie/chrome-profile/SingletonLock'
# Let the LaunchAgent's supervisor restart on its own (10s timer)
# Or force a restart immediately:
ssh <sink> 'launchctl kickstart -k gui/$UID/dev.agentcookie.sink'
```

The related failure path is "Chrome.app missing": the wizard's `CDP injector` doctor check WARNs when `/Applications/Google Chrome.app` and `~/Applications/Google Chrome.app` both do not exist. Install Chrome stable, or set `cdp.chrome_binary` in `sink.yaml` to a non-default path, or pass `--no-cdp` at wizard install to disable CDP injection entirely.

## DBSC-suspect cookies dropped or refresh-failing

`agentcookie doctor` reports `DBSC: last push flagged N shipped-with-warning, M skipped DBSC-suspect cookie(s): <sample-domain>`. This means the source's heuristic flagged cookies that look like Chrome's Device Bound Session Credentials — short-lived secure cookies whose refresh requires signing a server challenge with the source Mac's Secure Enclave private key.

| Posture                            | Source flag                                   | Sink effect                                                                 |
| ---------------------------------- | --------------------------------------------- | --------------------------------------------------------------------------- |
| Ship-with-warning (default)        | none                                          | Cookie reaches the sink but expires within ~minutes; agent gets logged out  |
| Hard-skip                          | `--skip-dbsc-suspect` / `AGENTCOOKIE_SKIP_DBSC_SUSPECT=1` | Cookie never leaves the source; sink never sees it                          |

The heuristic keys on two signals (`internal/chrome/dbsc.go`):

- A secure cookie on a known DBSC host (currently the `google.com` suffix).
- A secure persistent cookie with remaining TTL < 15 minutes.

Recommended handling per the FAQ: for Google sessions, do not copy cookies at all — sign the sink's Chrome into the same Google account once and it establishes its own device-bound session locally. For every other site, the default ship-with-warning is correct; a false positive does not break a working non-DBSC site.

To opt the LaunchAgent into hard-skip without editing the source plist:

```bash
launchctl setenv AGENTCOOKIE_SKIP_DBSC_SUSPECT 1
launchctl kickstart -k gui/$UID/dev.agentcookie.source
```

## Sink never accepted a write

Doctor: `Sink state: sink-state.json missing; sink has never accepted a write` (FAIL) or `last write Xh ago (>24h)` (WARN). The remediation is the same: force a real round-trip.

```bash
# On the source, push once:
agentcookie source --once

# Then re-check both sides:
agentcookie status --json
ssh <sink> 'agentcookie status --json'
```

If `agentcookie status` reports zero pushes after install, the source watcher has not seen a Chrome write yet. Open any allowlisted tab on the source's Chrome and refresh — a push should appear within ~2 seconds.

## Adapter coverage gaps

Doctor: `Adapter coverage: N of M host_keys in sidecar have no adapter: <host1>, <host2>, ...` (WARN).

This is not a failure. The plaintext sidecar at `~/.agentcookie/cookies-plain.db` always covers sidecar-aware PP CLIs. The WARN only matters for `kooky`-only readers that do not link `pkg/sidecar`, and those fall back to Chrome's encrypted store anyway. Inspect with:

```bash
agentcookie wizard verify-adapters           # one row per registered adapter
agentcookie wizard verify-adapters --json    # for SSH agents
```

Output is a tabular `ADAPTER STATUS PUSHED DETAIL` snapshot from `sink-state.json`'s `LastAdapterResults`. Exit code is `1` if any adapter reported an error in the most recent run.

## FAQ

<AccordionGroup>
<Accordion title="Will syncing cookies log me out of sites on the source machine?">
No. The source reads cookies with `immutable=1` (Chrome's recommended read-only flag), and `agentcookie source` never writes to the source's Cookies SQLite. The only writes happen on the sink.
</Accordion>

<Accordion title="My agent gets logged out on a particular site after syncing — what happened?">
Two causes. First, a few sites bind a session to a device fingerprint (canvas, screen size, accept-language, sometimes TLS JA3); replicating the cookie alone is not enough and the site invalidates the session. Second, the site uses Chrome's DBSC, which ties session refresh to a private key in the source Mac's Secure Enclave. Workarounds: remove the site from your allowlist and re-auth in the sink's Chrome directly, or use a pair-agent remote-browser pattern for those sites.
</Accordion>

<Accordion title="Does Chrome's DBSC break agentcookie?">
No, not for the sites you use today. DBSC is opt-in per site; as of May 2026 the one broad adopter is Google's own account/Workspace cookies. The secrets bus is untouched — bearer tokens, API keys, and OAuth refresh tokens replicate normally. For Google sessions, sign the sink's Chrome into the same account once and it establishes its own device-bound session locally.
</Accordion>

<Accordion title="Why is the sink's allowlist independent from the source's?">
Defense in depth. The source filters before sending (bandwidth + privacy optimization), but the sink owner ultimately controls what state lands in their Chrome. If the source is compromised and an attacker tries to push cookies for new domains, the sink-side allowlist drops them. Keep both allowlists in sync for the simplest behavior; let them diverge if you want the sink to be more conservative.
</Accordion>

<Accordion title="What about durable replay defense?">
In v0.1, the sink rejects an envelope whose Sequence is not strictly greater than the highest seen for that source — but the state is in-memory, so a sink restart clears it and an attacker who captured a payload could replay it once before the next legitimate sync. v0.2 adds a nonce-or-timestamp window for durable replay defense.
</Accordion>

<Accordion title="Is the `shared_secret` fallback safe?">
Safer than nothing if you use a strong randomly-generated secret and never reuse it. Strictly worse than pairing: the secret sits in two YAML files unencrypted (file mode 0600), rotation requires editing both files, and the MITM defense (pairing code in HKDF salt) is missing. After pairing once, delete `security.shared_secret` from both YAMLs.
</Accordion>

<Accordion title="Can I run the sink in Docker on a cloud VM?">
The sink can run anywhere Chrome stable runs (when CDP is enabled and Chrome is reachable) OR anywhere you can mount the destination's Chrome Cookies SQLite. Linux sink support is on the roadmap. On macOS-in-the-cloud (e.g. MacStadium), it works today if you can SSH in and the tailnet reaches the host.
</Accordion>

<Accordion title="What's in the keystore on disk?">
`~/.config/agentcookie/keys/<peer>.json` is a JSON file at mode `0600` containing the 32-byte paired key (base64), the peer hostname, paired_at timestamp, key fingerprint, and protocol version. macOS Keychain storage is a v0.2 hardening item; today the file mode + OS user separation is the protection.
</Accordion>

<Accordion title="Why not just use a Chrome extension to sync cookies?">
Existing extensions assume a human will click "Merge" or open Chrome periodically. agentcookie's target is the opposite: continuous one-way replication from a laptop to a Mac mini or cloud VM where AI agents act with no human in the loop on the sink side. agentcookie covers the agent-operator workflow without requiring a browser running on the sink at all.
</Accordion>
</AccordionGroup>

## Related pages

<CardGroup>
<Card title="doctor and adapter verification" href="/doctor-health-checks">Every check category, the JSON envelope, and exit-code semantics agents consume.</Card>
<Card title="Headless second-Mac install" href="/headless-install">The SSH-only install path, degraded-mode fallback, and the one-password Safe Storage open.</Card>
<Card title="Device-bound cookies (DBSC)" href="/dbsc-handling">The detection heuristic, default ship-with-warning posture, and `--skip-dbsc-suspect` drop path.</Card>
<Card title="LaunchAgent management" href="/launchagent-management">Plist locations, log paths, and the v0.9 lifecycle the doctor remediations reference.</Card>
<Card title="Pairing and per-peer keys" href="/pairing-and-keys">The X25519 + HKDF handshake, per-peer key file, and rate-limited `/pair` endpoint.</Card>
<Card title="Universal cookie delivery" href="/universal-delivery">The one-login-password Safe Storage open and duplicate-Keychain-item convergence.</Card>
</CardGroup>
