# CLI reference

> Every `agentcookie` subcommand and its flags: `source`, `sink`, `pair`, `wizard`, `doctor`, `status`, `secret`, `discover`, `cookies`, `version`.

- 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

- `internal/cli/root.go`
- `internal/cli/source.go`
- `internal/cli/sink.go`
- `internal/cli/pair.go`
- `internal/cli/wizard.go`
- `internal/cli/secret.go`
- `internal/cli/cookies.go`
- `internal/cli/discover.go`

---

---
title: "CLI reference"
description: "Every `agentcookie` subcommand and its flags: `source`, `sink`, `pair`, `wizard`, `doctor`, `status`, `secret`, `discover`, `cookies`, `version`."
---

`agentcookie` is a single cobra-rooted binary defined in `internal/cli/root.go`. The root command registers ten subcommands — `source`, `sink`, `pair`, `status`, `version`, `wizard`, `doctor`, `secret`, `discover`, `cookies` — plus a hidden `internal` group. Two persistent flags apply to every invocation, and every subcommand that emits structured output respects `--json`.

## Persistent flags

These are declared on `rootCmd` and apply to every subcommand.

<ParamField body="--config-dir" type="string" default="~/.config/agentcookie">
  Directory holding `source.yaml`, `sink.yaml`, `allowlist.yaml`, `blocklist.yaml`, and the paired-key directory `keys/<peer>.json`.
</ParamField>

<ParamField body="--json" type="bool" default="false">
  Emit a machine-readable JSON document where the subcommand supports it (`source --once`, `status`, `doctor`, `secret list`, `discover`, `version`, `cookies`, `wizard verify-adapters`).
</ParamField>

## Subcommand inventory

| Subcommand | Purpose |
| --- | --- |
| `source` | Read allowlisted cookies + secrets and push to the sink (`--once` or `--watch`). |
| `sink` | Long-lived HTTP listener that decrypts envelopes and writes Chrome state. |
| `pair` | X25519 + HKDF-SHA256 pairing handshake; writes `keys/<peer>.json`. |
| `wizard` | One-command install/uninstall and operational subcommands (`set-keychain-access`, `verify-adapters`). |
| `doctor` | Runs 15 health checks and emits `DoctorReport` JSON. |
| `status` | Prints loaded config + last-known daemon state. |
| `secret` | Manage the per-CLI secrets bus (`list`, `get`, `set`, `rm`, `env`, `alias`, `import-from`, `revoke`). |
| `discover` | List adopted projects from the v2 secrets-bus registry. |
| `cookies` | Print synced cookies for a domain from the local sidecar; keychain-free. |
| `version` | Print the linker-injected version string. |

## source

```text
agentcookie source --once    # one read+push cycle, then exit
agentcookie source --watch   # long-lived fsnotify watcher (LaunchAgent default)
```

Exactly one of `--once` or `--watch` is required. `--watch` debounces Chrome `Cookies` writes by 500 ms, rate-caps pushes at one per 2 seconds, and additionally watches `~/.agentcookie/secrets/` and the v2 discovery roots so a new `secrets.env` or a freshly discovered manifest fires the same push pipeline.

<ParamField body="--once" type="bool">
  Single read + filter + push cycle. Bound by the `SyncClient` timeout plus a 30 s slack.
</ParamField>
<ParamField body="--watch" type="bool">
  Long-running fsnotify watcher across Chrome Cookies, Local Storage, IndexedDB, the secrets bus, and the v2 discovery roots.
</ParamField>
<ParamField body="--verbose" type="bool">
  Log per-pattern decisions and DBSC detail to stderr.
</ParamField>
<ParamField body="--dry-run" type="bool">
  Read + filter but do not contact the sink.
</ParamField>
<ParamField body="--skip-dbsc-suspect" type="bool">
  Drop cookies that look device-bound (DBSC) instead of shipping them with a warning. Also honored via `AGENTCOOKIE_SKIP_DBSC_SUSPECT=1`.
</ParamField>

Environment toggles read by `source`:

- `AGENTCOOKIE_SKIP_DBSC_SUSPECT=1` — same as `--skip-dbsc-suspect`.
- `AGENTCOOKIE_SYNC_INDEXEDDB=1` — opt in to packing the IndexedDB tarball (off by default; per-file 5 MiB cap with skipped-list reporting).

`--json` (root flag) emits the per-push result map:

```json
{
  "cookies_read": 1240,
  "cookies_blocked": 12,
  "cookies_passing": 1228,
  "cookies_dbsc_warned": 0,
  "cookies_dbsc_skipped": 0,
  "secrets_clis": 3,
  "dry_run": false,
  "sink_url": "http://mini:9999/sync",
  "posted": true,
  "sink_response": "ok: wrote ...",
  "sink_status": 200
}
```

## sink

```text
agentcookie sink              # long-lived HTTP listener
agentcookie sink --dry-run    # accept + decrypt only; dump batches to stderr
```

The sink reads `sink.yaml`, validates `listen.addr` against the v0.12 binding policy (tailnet 100.x or an explicit loopback; `0.0.0.0` is rejected), and serves two HTTP routes:

- `GET /healthz` → `ok`.
- `POST /sync` → accepts an AES-256-GCM-sealed `SyncEnvelope`, runs sequence/blocklist/version checks, then either writes Chrome SQLite + LevelDB + IndexedDB (legacy path) or sidecar-only (`skip_chrome_sqlite: true`), runs registered `sinkpush` adapters, optionally invokes CDP injection, and persists secrets-bus payloads to `~/.agentcookie/secrets/<cli>/`.

<ParamField body="--dry-run" type="bool">
  Skip Chrome Safe Storage, SQLite, LevelDB, IndexedDB, sidecar, and adapter writes. Each accepted batch is dumped to stderr as JSON. Useful for debugging the wire format and for running the sink over SSH without the GUI Keychain prompt.
</ParamField>

The sink does not accept `--listen` on the command line; the bind address is fixed in `sink.yaml` so that the wizard's resolved tailnet address is the durable record.

<Warning>
The sink refuses to start when `sink.yaml`'s `listen.addr` is `0.0.0.0`, `::`, an empty host, or a non-tailnet/non-loopback IP. Run `tailscale status` for a 100.x address or re-run `agentcookie wizard install --as sink`.
</Warning>

## pair

```text
agentcookie pair --as source        # on the source machine
agentcookie pair --as sink \\
  --peer <source-hostname> \\
  --pair-url http://<source>:9998/pair \\
  --code <code>                     # on the sink machine
```

`pair` performs the X25519 + HKDF-SHA256 handshake and writes the derived 32-byte key to `<config-dir>/keys/<peer>.json` with mode 0600. The same peer hostname must also appear in `source.yaml`/`sink.yaml` `peer.hostname`, so the sync runtime looks up the key by filename.

<ParamField body="--as" type="enum" required>
  `source` or `sink`. Required.
</ParamField>
<ParamField body="--listen" type="host:port">
  Source-only. Empty triggers tailnet auto-detection (`100.x:9998`). Explicit values must satisfy the tailnet-or-loopback policy; `0.0.0.0` is refused.
</ParamField>
<ParamField body="--local-name" type="string" default="os.Hostname()">
  Hostname this side announces to the peer.
</ParamField>
<ParamField body="--pair-url" type="url">
  Sink-only. Full URL of the source's `/pair` endpoint.
</ParamField>
<ParamField body="--code" type="string">
  Sink-only. Base32 pairing code printed by the source.
</ParamField>
<ParamField body="--peer" type="string">
  Sink-only. Source machine's hostname; also the filename for the derived key.
</ParamField>

## wizard

`wizard` is a subcommand group. Four child commands ship:

| Command | Purpose |
| --- | --- |
| `install` | Drop configs, run the pair handshake, install a LaunchAgent. |
| `uninstall` | Remove the LaunchAgent; optional `--purge` deletes configs and paired keys. |
| `set-keychain-access` | Grant agentcookie + named binaries access to Chrome Safe Storage. |
| `verify-adapters` | Print the most recent `sinkpush` adapter results from `sink-state.json`. |

### wizard install

```text
agentcookie wizard install --as source --peer <sink-hostname>
agentcookie wizard install --as sink   --peer <source-hostname> \
                                       --code <pairing-code>    \
                                       --pair-url <source-pair-url>
```

<ParamField body="--as" type="enum" required>
  `source` or `sink`.
</ParamField>
<ParamField body="--peer" type="string" required>
  The OTHER machine's hostname.
</ParamField>
<ParamField body="--listen" type="host:port">
  Source-only pairing listener bind address. Empty → tailnet auto-detect.
</ParamField>
<ParamField body="--local-name" type="string" default="os.Hostname()">
  Hostname this side announces.
</ParamField>
<ParamField body="--sink-url" type="url">
  Source-only override for the sink URL (default `http://<peer>:9999/sync`).
</ParamField>
<ParamField body="--code" type="string">
  Sink-only pairing code from the source's wizard output.
</ParamField>
<ParamField body="--pair-url" type="url">
  Sink-only source pair URL.
</ParamField>
<ParamField body="--repair" type="bool">
  Force a fresh pairing handshake even if a key already exists.
</ParamField>
<ParamField body="--force" type="bool">
  Overwrite existing `source.yaml`, `sink.yaml`, `blocklist.yaml`. Also reconciles a `peer.hostname` mismatch.
</ParamField>
<ParamField body="--skip-daemon" type="bool">
  Skip installing the LaunchAgent (configs + pairing only).
</ParamField>
<ParamField body="--skip-exit-node-hint" type="bool">
  Do not detect Tailscale or print the `tailscale set --advertise-exit-node` / `--exit-node=` suggestions.
</ParamField>
<ParamField body="--skip-keychain-prompt" type="bool">
  Sink-only. Do not trigger the Chrome Safe Storage Keychain prompt during install; the sink daemon prompts on first sync instead.
</ParamField>
<ParamField body="--skip-partition-list" type="bool">
  Sink-only. Do not expand the Chrome Safe Storage Keychain partition list.
</ParamField>
<ParamField body="--skip-keychain-access" type="bool">
  Sink-only. Do not run the v0.10 `set-keychain-access` strategies.
</ParamField>
<ParamField body="--skip-bridge-hint" type="bool">
  Sink-only. Do not print the cookie-bridge env-var integration hint.
</ParamField>
<ParamField body="--skip-chrome-sqlite" type="bool">
  Sink-only opt-out of universal delivery: the sink never reads Chrome Safe Storage or writes Chrome SQLite/leveldb. Forces degraded mode and overrides `--write-chrome-sqlite` when both are passed.
</ParamField>
<ParamField body="--write-chrome-sqlite" type="bool">
  Sink-only. Force universal delivery and honor it even if the keychain open cannot complete; does not silently downgrade.
</ParamField>
<ParamField body="--no-cdp" type="bool">
  Sink-only. Do not enable CDP injection alongside `skip_chrome_sqlite`. Default headless installs enable CDP injection.
</ParamField>

The v0.13 default for `wizard install --as sink` is universal delivery regardless of TTY. The keychain open runs before `sink.yaml` is rendered; on a default install where the open fails, the wizard non-fatally downgrades to degraded (skip + CDP) and prints the one-line upgrade instruction.

### wizard uninstall

```text
agentcookie wizard uninstall --as source [--purge]
agentcookie wizard uninstall --as sink   [--purge]
```

<ParamField body="--as" type="enum" required>
  `source` or `sink`. Removes the matching LaunchAgent plist via `launchctl`.
</ParamField>
<ParamField body="--purge" type="bool">
  Also delete `source.yaml`, `sink.yaml`, `allowlist.yaml`, and the `keys/` directory.
</ParamField>

### wizard set-keychain-access

Broadens Chrome Safe Storage access so unmodified cookie tools and kooky-CGO PP CLIs read without per-binary prompts.

```text
agentcookie wizard set-keychain-access                 # default: inline one-password partition
AGENTCOOKIE_LOGIN_PASSWORD=… agentcookie wizard set-keychain-access   # non-interactive
agentcookie wizard set-keychain-access --recreate      # legacy LaunchAgent strategy chain
agentcookie wizard set-keychain-access --any-app       # delete-and-recreate with -A
agentcookie wizard set-keychain-access --extra-binary /Users/x/go/bin/foo-pp-cli
```

<ParamField body="--extra-binary" type="path[]">
  Absolute path to a kooky-using CLI binary; added to the trust-list fallback. Repeatable.
</ParamField>
<ParamField body="--any-app" type="bool">
  Recreate Chrome Safe Storage with `-A` (any application). Preserves the existing key value. Security: any local process can then read Chrome cookies.
</ParamField>
<ParamField body="--recreate" type="bool">
  Use the legacy LaunchAgent delete-and-recreate trust-list strategy chain instead of the inline one-password partition default.
</ParamField>

Hidden flags retained for the wizard and tests: `--inner-runner` (runs the strategy loop in this process when invoked from a one-shot LaunchAgent) and `--enable-sealing` (creates the `agentcookie-master` Keychain item used by sidecar / adapter writers).

`AGENTCOOKIE_LOGIN_PASSWORD=…` substitutes for the interactive login-password prompt so the command runs cleanly over SSH.

### wizard verify-adapters

Prints one row per registered `sinkpush` adapter from `~/.agentcookie/sink-state.json`. With the root `--json` flag, emits an envelope of `state.AdapterResult`s and a `status` of `ok`, `no_state`, or `no_runs`. Exits non-zero only when at least one recorded adapter result has a non-empty `err`.

## doctor

```text
agentcookie doctor          # human output
agentcookie doctor --json   # DoctorReport envelope
```

`doctor` runs 15 checks in order. Each row carries a `Severity` of `ok`, `warn`, `fail`, `info`, or `skipped`. Exit code is `0` only when no check is `fail`.

| # | Name | Notes |
| --- | --- | --- |
| 1 | Binary signature | Looks for Developer ID team `NM8VT393AR` via `codesign -d -r-`. |
| 2 | Tailscale | Requires a tailnet 100.x address; FAIL otherwise. |
| 3 | Config | Parses `source.yaml` and/or `sink.yaml`. |
| 4 | Keystore | Confirms `keys/<peer>.json` exists with mode 0600. |
| 5 | Sink listener | Tries to bind the configured `listen.addr`; bind success = sink not running = FAIL. |
| 6 | Sink state | Freshness of `sink-state.json`. |
| 7 | Source state | Freshness of `source-state.json`. |
| 8 | DBSC | Count of DBSC-suspect cookies in the last push. |
| 9 | Sealing | Reports presence of the `agentcookie-master` Keychain item. |
| 10 | Adapter coverage | Sidecar host_keys not matched by any registered adapter. |
| 11 | CDP injector | Verifies `cdp.profile_dir` exists and Chrome.app is installed. |
| 12 | Secrets bus | Counts CLIs, keys, sealed/plaintext mode, newest mtime. |
| 13 | Secret coverage | Flags CLIs whose synced store does not provide the declared auth env var. |
| 14 | Binary install | Detects multiple diverging `agentcookie` binaries on the machine. |
| 15 | Cookie delivery | Reports universal vs degraded delivery and probes Chrome Safe Storage. |

### DoctorReport (`--json`)

```json
{
  "version": "0.13.0",
  "exit_code": 0,
  "checks": [
    {
      "name": "Tailscale",
      "severity": "ok",
      "detail": "100.64.0.5 reachable on local tailnet interface"
    }
  ]
}
```

`Severity` field constants: `ok`, `warn`, `fail`, `info`, `skipped`. `Remediation` is populated for `warn`/`fail` rows and rendered as a hanging line in the human output.

## status

```text
agentcookie status
agentcookie status --json
```

Loads `source.yaml`, `sink.yaml`, `allowlist.yaml`, and the last-known `source-state.json` + `sink-state.json` and prints a compact summary. Parse errors land in the JSON envelope's `errors` array and are echoed to stderr in human mode. The JSON envelope shape:

```json
{
  "version": "...",
  "config_dir": "/Users/x/.config/agentcookie",
  "source_config": { "...": "..." },
  "sink_config":   { "...": "..." },
  "allowlist":     { "...": "..." },
  "source_state":  { "...": "..." },
  "sink_state":    { "...": "..." },
  "errors":        []
}
```

## secret

`secret` manages the per-CLI secrets bus under `~/.agentcookie/secrets/<cli-name>/`. CLI names match `[a-z0-9-]{1,64}` (no leading/trailing dash); keys match identifier syntax (`[A-Za-z_][A-Za-z0-9_]*`).

| Subcommand | Arguments | Behavior |
| --- | --- | --- |
| `list` | — | Print every registered CLI and its key names (no values). `--json` returns `[{name, keys[]}]`. |
| `get` | `<cli> <key>` | Print the value to stdout. Sealed twin (`secrets.env.sealed`) wins over plaintext. |
| `set` | `<cli> <key>` | Read value from stdin (pipe) or prompt (TTY); rewrites `secrets.env` atomically with mode 0600. |
| `rm` | `<cli> [<key>]` | Remove a single key or the whole CLI directory. |
| `env` | `<cli>` | Print all keys as `KEY=VALUE` lines, with `aliases.env` and manifest aliases applied. Suitable for `eval $(agentcookie secret env <cli>)`. |
| `alias` | `<cli> [<declared> <stored>]` | Map a consumer's declared env var to a stored key. List with one arg. Resolved live by `secret env`. |
| `import-from` | `<path> --as <cli>` | Ingest a JSON, TOML, or env file, applying canonical field-name heuristics. |
| `revoke` | `<name> [--force]` | Undo project adoption based on its `RegisteredProject.Kind`. |

### secret import-from

<ParamField body="--as" type="string" required>
  CLI name to file the imported secrets under.
</ParamField>

Supported extensions: `.env`, `.json`, `.toml`. JSON/TOML are flattened. Field-name canonicalization includes `access_token` → `OAUTH_BEARER`, `refresh_token` → `OAUTH_REFRESH`, `api_key` → `API_KEY`, `client_id` → `OAUTH_CLIENT_ID`, `client_secret` → `OAUTH_CLIENT_SECRET`, `bearer` → `OAUTH_BEARER`, `token` → `TOKEN`, `auth_header` → `AUTH_HEADER`, `(token_)expiry/expires_at` → `OAUTH_EXPIRES_AT`, `base_url` → `BASE_URL`. Unknown but env-shaped keys are upper-cased; otherwise prefixed `_unknown_` for review.

### secret revoke

<ParamField body="--force" type="bool">
  Skip the confirmation prompt when deleting an explicit-manifest project's `agentcookie.toml`.
</ParamField>

Behavior depends on the discovered adoption tier:

- `explicit-manifest` — deletes the manifest file (requires `--force`).
- `pp-cli-derived` — prints the `sync.default = false` manifest stub to drop; never touches the PP CLI itself.
- `legacy-v1` — `rm -rf` of the bus directory under `~/.agentcookie/secrets/<name>/`.

## discover

```text
agentcookie discover
agentcookie discover -v       # include skipped manifests + discovery errors
agentcookie discover --json
```

<ParamField body="--verbose, -v" type="bool">
  Include skipped manifests with the reason and any discovery errors.
</ParamField>

Human output is a `tabwriter` table with columns `NAME`, `TIER`, `READ-IN-PLACE`, `KEYS`, `COVERAGE`. CLIs whose synced secrets do not match the declared auth env var print a `MISMATCH` coverage status and a follow-up block listing the gaps. `--json` emits:

```json
{
  "projects": [
    {
      "name": "tesla-pp-cli",
      "kind": "explicit-manifest",
      "source_path": "/Users/x/.agentcookie/manifests/tesla-pp-cli.toml",
      "read_in_place_path": "/Users/x/.config/tesla-pp-cli/auth.json",
      "display_name": "Tesla PP CLI",
      "key_count": 4,
      "sync_summary": "default=true, 0 overrides",
      "coverage": "OK"
    }
  ],
  "skipped": []
}
```

`tier` values are the `secretsbus.RegisteredProject.Kind` strings: `explicit-manifest`, `pp-cli-derived`, `legacy-v1`.

## cookies

```text
agentcookie cookies --domain .amazon.com
agentcookie cookies --domain .amazon.com --json
```

<ParamField body="--domain" type="string" required>
  Cookie domain to fetch. A leading dot is stripped; both `amazon.com` and `www.amazon.com` host_keys match `.amazon.com`, but `evilamazon.com` never does.
</ParamField>

Reads the local plaintext sidecar (`~/.agentcookie/cookies-plain.db`) via `pkg/sidecar.ReadSidecar`, which transparently unseals `agc1:` values when sealing is on. Applies the local `blocklist.yaml` so a blocked domain never leaks out. Default output is a single-line `Cookie:`-header-shaped string (`name=value; name=value`). With `--json`, an array of `{name, value, domain, path, secure}`.

A missing sidecar is not an error — `cookies` exits 0 with empty output so a caller can fall through to its own auth path.

## version

```text
agentcookie version
agentcookie version --json   # {"version":"..."}
```

The version string is set via `-ldflags '-X github.com/mvanhorn/agentcookie/internal/cli.Version=…'` at link time and defaults to `0.0.1-dev` for `go run`/dev builds.

## Hidden commands

`agentcookie internal` is hidden from `--help`. The only documented child is `internal keychain-probe`, which calls Chrome Safe Storage via the same kooky-CGO Keychain API path and prints `ok len=N` or `fail: <error>`. It accepts `--timeout-seconds <int>` (default 5).

## Common error and exit semantics

| Surface | Failure | Behavior |
| --- | --- | --- |
| `source` / `sink` | `0.0.0.0` or non-tailnet listen address | Refuses to start; prints the v0.12 binding policy message. |
| `source` | No paired key, no `security.shared_secret` | Errors with `no transport credential available`. |
| `sink` | `POST /sync` fails AES-GCM open | Responds `401`. |
| `sink` | Envelope protocol version out of range | Responds `400`. |
| `sink` | Sequence ≤ last seen for source | Responds `409` (replay defense). |
| `doctor` | Any check is `fail` | Exit code 1; remediation lines are printed under each non-OK row. |
| `wizard verify-adapters` | Any adapter recorded `err != ""` | Exit code 1. |

## Related pages

<CardGroup>
  <Card title="Configuration files reference" href="/config-files-reference">
    Schemas for `source.yaml`, `sink.yaml`, `allowlist.yaml`, and `blocklist.yaml`, including the tailnet-only listen check.
  </Card>
  <Card title="doctor and adapter verification" href="/doctor-health-checks">
    Full check categories, `DoctorReport` JSON shape, and `wizard verify-adapters` semantics.
  </Card>
  <Card title="Pairing and per-peer keys" href="/pairing-and-keys">
    Details on the X25519 + HKDF-SHA256 handshake `pair` performs and the on-disk key format.
  </Card>
  <Card title="LaunchAgent management" href="/launchagent-management">
    What `wizard install` writes under `~/Library/LaunchAgents/` and how the daemons are bootstrapped.
  </Card>
  <Card title="Secrets bus" href="/secrets-bus">
    What the `secret`, `discover`, and source-side bus push pipelines move between peers.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Recovery paths for pairing `connection refused`, sink Keychain prompts, stale `SingletonLock`, DBSC drops, and other CLI surfaces.
  </Card>
</CardGroup>
