# Installation

> Prerequisites (Tailscale, Chrome, Go 1.22+), `go install` of the unified CLI, and the `agentcookie wizard install` flow that drops configs, pairs, and installs the LaunchAgent.

- 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

- `README.md`
- `internal/cli/wizard.go`
- `internal/cli/wizard_keychain.go`
- `scripts/install-beta.sh`
- `Makefile`

---

---
title: "Installation"
description: "Prerequisites (Tailscale, Chrome, Go 1.22+), `go install` of the unified CLI, and the `agentcookie wizard install` flow that drops configs, pairs, and installs the LaunchAgent."
---

`agentcookie` ships as a single Go binary (`github.com/mvanhorn/agentcookie/cmd/agentcookie`) installed identically on the source Mac and the sink Mac. Bringing a pair online is two commands: `go install ...@latest` to place the binary, then `agentcookie wizard install` on each side. The wizard drops `source.yaml` / `sink.yaml`, runs the pairing handshake (port `9998`), attempts the one-password Chrome Safe Storage open, and bootstraps the per-role LaunchAgent (`dev.agentcookie.source` / `dev.agentcookie.sink`) so all subsequent sync work runs unattended.

## Prerequisites

The wizard refuses to write a permissive listener default — Tailscale must be running so it can pin a tailnet 100.x address before rendering YAML. Chrome is only strictly required on the source for cookie reads, but the install scripts also probe for it on the sink.

<ParamField body="Tailscale" type="daemon" required>
Running and signed in on both Macs. The wizard calls `tsclient.RequireTailnetIP` to detect a 100.x address for the pairing listener (source) and the `/sync` listener (sink). A missing or unreachable Tailscale daemon causes `wizard install` to abort with `detect Tailscale 100.x address for ... listener`.
</ParamField>

<ParamField body="Google Chrome (stable)" type="application" required>
Installed on the source — the source watcher reads `~/Library/Application Support/Google/Chrome/Default/Cookies` via the Chrome Safe Storage key. On the sink, Chrome is required only when universal delivery is in use (the default).
</ParamField>

<ParamField body="Go toolchain" type="1.22+">
Needed only when installing from source. Pre-built release tarballs (`agentcookie-<version>-darwin-arm64.tar.gz`) can be used instead via `scripts/install-beta.sh --tarball <path>`.
</ParamField>

<ParamField body="macOS" type="14+ (Apple silicon)">
Source and sink both run macOS. The sink relies on macOS LaunchAgent + Keychain conventions; both sides use the macOS-only Chrome Safe Storage decrypt path.
</ParamField>

<Note>
The README pins Go 1.22+ as the public floor; `go.mod` currently targets `go 1.26.2`. Use a recent toolchain (`go install` will fetch a matching one when required) or install from a release tarball to skip the toolchain entirely.
</Note>

## Install the binary

Run on both Macs. `go install` deposits `agentcookie` in `$(go env GOBIN)` (default `$HOME/go/bin`).

```bash
go install github.com/mvanhorn/agentcookie/cmd/agentcookie@latest
```

<Tip>
If `~/go/bin` is not on the sink's `PATH`, the LaunchAgent still works (it uses absolute paths) but `ssh sink 'agentcookie ...'` will report `command not found`. Add `export PATH="$HOME/go/bin:$PATH"` to the sink shell profile.
</Tip>

### Build from source

The repo's `Makefile` provides a signing-aware local build for contributors and release builds:

| Target | Result |
|---|---|
| `make build` | `go build -o bin/agentcookie ./cmd/agentcookie` |
| `make install` | `go install ./cmd/agentcookie`, then `scripts/sign.sh "$(go env GOBIN)/agentcookie"` |
| `make` / `make all` | Build + sign in `bin/agentcookie` |
| `make release` | Build + sign + notarize (a portable binary that launches without prompts on any Mac) |
| `make verify` | `codesign -d -r-` of the local build |
| `make test` | `go test -race ./...` |

`make sign` / `make notarize` require an Apple Developer ID. Override the identity via `AGENTCOOKIE_SIGN_IDENTITY`. Plain contributors can stop at `make build` + `make test` without a cert.

### Release tarball (beta installer)

For closed-beta or air-gapped installs, `scripts/install-beta.sh` automates fetch + Gatekeeper / Developer ID verification + binary placement + wizard launch:

```bash
./install-beta.sh --as source                    # uses gh release download
./install-beta.sh --as sink   --tarball ./bundle.tar.gz \
                              --code <pairing-code> \
                              --pair-url http://<source>:9998/pair
```

The script:

- Checks Tailscale is up and the Chrome `.app` is installed.
- Downloads `*darwin-arm64.tar.gz` via `gh release download` (or accepts `--tarball`).
- Runs `codesign --verify --strict` and confirms the Developer ID OU is `NM8VT393AR`.
- Places the binary in `/usr/local/bin/agentcookie` if writable, otherwise `$HOME/bin/agentcookie`.
- Forwards `--peer`, `--code`, `--pair-url`, `--extra-binary`, `--skip-chrome-sqlite`, `--write-chrome-sqlite`, `--no-cdp`, and `--skip-keychain-prompt` to `agentcookie wizard install`.
- Auto-adds `--skip-keychain-prompt` on a sink install when stdin is not a TTY.
- Ends with `agentcookie doctor` so the operator sees the post-install health roll-up.

## Pair and install with the wizard

`agentcookie wizard install` is the single command per machine. The source side runs first because it generates the pairing code the sink consumes.

<Steps>
<Step title="Source: drop configs and start pairing listener">
On the laptop you browse from:

```bash
agentcookie wizard install --as source --peer <sink-hostname>
```

The source wizard, in order:

1. Validates `--as` (`source` or `sink`) and `--peer` (the OTHER machine's tailnet hostname).
2. Creates `~/.config/agentcookie/` (mode `0755`) and writes `source.yaml` + `blocklist.yaml` with `0600` perms if missing (use `--force` to overwrite an existing pair). `source.yaml` points at `http://<peer>:9999/sync` by default.
3. Refuses to proceed when an existing `source.yaml` has a `peer.hostname` that disagrees with `--peer` (the `guardConfigPeerMismatch` check; pass `--force` to overwrite).
4. Resolves a Tailscale 100.x address via `tsclient.RequireTailnetIP` and binds the pairing HTTP listener at `100.x.y.z:9998`. Plain `0.0.0.0`, `::`, and other any-interface binds are refused by `validateListenAddr`.
5. Prints the pairing code to stderr **and** writes a JSON sibling file `~/.agentcookie/pairing.json` so an SSH-driven agent can read the exact sink-side invocation:
   ```json
   {
     "code": "YILU-OIVK",
     "peer": "my-laptop",
     "pair_url": "http://100.x.y.z:9998/pair",
     "sink_run": "agentcookie wizard install --as sink --peer my-laptop --code YILU-OIVK --pair-url http://100.x.y.z:9998/pair"
   }
   ```
6. Blocks until the sink connects, then saves the per-peer key at `~/.config/agentcookie/keys/<peer>.json` (filed under the operator-supplied `--peer`, not the sink's announced hostname).
7. Installs the `dev.agentcookie.source` LaunchAgent invoking the resolved binary with `--watch`. Logs land in `~/.agentcookie/logs/`.

If a key for `--peer` already exists, the pairing step is skipped. Pass `--repair` to force a fresh handshake.
</Step>

<Step title="Sink: paste the code and complete the handshake">
On the second Mac:

```bash
agentcookie wizard install --as sink \
  --peer <source-hostname> \
  --code <pairing-code> \
  --pair-url http://<source-host>:9998/pair
```

`--code` and `--pair-url` are required when `--as sink`. The sink wizard then:

1. Resolves the universal-vs-degraded delivery mode (see below) and runs the Chrome Safe Storage open BEFORE rendering `sink.yaml`, so a failure cleanly downgrades the rendered config.
2. Resolves the sink's tailnet 100.x address and writes `sink.yaml` with `listen.addr: 100.x.y.z:9999`, `peer.hostname: <source>`, and the resolved `delivery:` marker (`universal` / `degraded`).
3. Runs `pairing.RunSink` against the source's `--pair-url`, derives the AES-256-GCM key, and saves `~/.config/agentcookie/keys/<peer>.json`.
4. Installs the `dev.agentcookie.sink` LaunchAgent. The daemon starts immediately under launchd.
5. Prints the optional Tailscale exit-node hint (`sudo tailscale set --exit-node=<source> --exit-node-allow-lan-access=true`) and the cookie-bridge env-var hint.

The wizard rejects `--code`/`--pair-url` mismatches at the `/pair` endpoint (HTTP 401), and the source's pair endpoint is rate-limited with a 64-bit code.
</Step>

<Step title="Verify">
Run on both Macs:

```bash
agentcookie doctor
agentcookie wizard verify-adapters
```

`doctor` walks fifteen health categories (binary signature, Tailscale, config, keystore, listener bind, sink/source state, sealing posture, adapter coverage, CDP injector, secrets-bus coverage, DBSC-suspect cookies). `verify-adapters` shows the per-adapter result of the last sync.
</Step>
</Steps>

## Cookie delivery mode resolved at install

On the sink, `resolveSinkHeadlessMode` + `resolveSinkDeliveryWithKeychain` decide what `sink.yaml` is rendered with and what runs against the Keychain. The v0.13 default is universal; degraded is the explicit opt-out path and is also the automatic fallback when the one-password open cannot complete.

| Sink flag | `skip_chrome_sqlite` | CDP | `delivery` marker | Keychain open attempted |
|---|---|---|---|---|
| (none) | `false` | `false` | `universal` | yes; on failure downgrades to degraded non-fatally |
| `--write-chrome-sqlite` | `false` | `false` | `universal` | yes; on failure WARNS and honors universal intent (no downgrade) |
| `--skip-chrome-sqlite` | `true` | `true` (unless `--no-cdp`) | `degraded` | no |
| `--skip-keychain-prompt` / `--skip-keychain-access` | `false` | `false` | `universal` | no |

In degraded mode the sink daemon never reads Chrome Safe Storage; cookie delivery uses the plaintext sidecar (`~/.agentcookie/cookies-plain.db`) + per-CLI adapter session files + CDP injection into a dedicated `~/.agentcookie/chrome-profile`. The upgrade-to-universal path is one command: `agentcookie wizard set-keychain-access`.

### Supplying the macOS login password non-interactively

The universal open prompts for the macOS login password once and never stores it. For headless / CI installs without a TTY, set the environment variable:

```bash
AGENTCOOKIE_LOGIN_PASSWORD='…' agentcookie wizard install --as sink \
  --peer <source> --code <code> --pair-url http://<source>:9998/pair
```

When neither the env var nor an interactive terminal is available, `acquireLoginPassword` returns `errNoInteractivePassword` and the wizard downgrades to degraded mode non-fatally, prints the upgrade command, and continues to install the LaunchAgent.

## `agentcookie wizard install` flag reference

<ParamField body="--as" type="source | sink" required>
Required. Selects the role. Source generates the code; sink consumes it.
</ParamField>

<ParamField body="--peer" type="hostname" required>
Required. The OTHER machine's tailnet hostname. Stored as `peer.hostname` in `source.yaml` / `sink.yaml` and as the filename for `~/.config/agentcookie/keys/<peer>.json`.
</ParamField>

<ParamField body="--listen" type="host:port">
Source pairing listener bind address. Defaults to `<tailnet 100.x>:9998`. `0.0.0.0` and other any-interface binds are refused; explicit `127.0.0.1`/`localhost` is allowed for local dev only.
</ParamField>

<ParamField body="--local-name" type="hostname">
Hostname this side announces during pairing. Defaults to `os.Hostname()`.
</ParamField>

<ParamField body="--sink-url" type="url">
Source-side override for the sink sync URL. Default: `http://<peer>:9999/sync`.
</ParamField>

<ParamField body="--code" type="string" required>
Sink-side: pairing code from the source's wizard output.
</ParamField>

<ParamField body="--pair-url" type="url" required>
Sink-side: source's pairing URL, e.g. `http://<source>:9998/pair`.
</ParamField>

<ParamField body="--repair" type="bool">
Force a fresh pairing handshake even if a key already exists for `--peer`.
</ParamField>

<ParamField body="--force" type="bool">
Overwrite existing `source.yaml` / `sink.yaml` / `blocklist.yaml`. Required to change `peer.hostname` in place.
</ParamField>

<ParamField body="--skip-daemon" type="bool">
Skip installing the LaunchAgent. Configs and pairing only.
</ParamField>

<ParamField body="--skip-exit-node-hint" type="bool">
Do not print the optional `sudo tailscale set --advertise-exit-node` / `--exit-node` hint.
</ParamField>

<ParamField body="--skip-chrome-sqlite" type="bool">
Sink: opt OUT of universal delivery. Renders `skip_chrome_sqlite: true`; the sink daemon never reads or writes Chrome's SQLite. Overrides `--write-chrome-sqlite` when both are passed.
</ParamField>

<ParamField body="--write-chrome-sqlite" type="bool">
Sink: force universal delivery and honor it even if the one-password keychain open cannot complete (does not silently downgrade).
</ParamField>

<ParamField body="--no-cdp" type="bool">
Sink: in degraded mode, do not enable CDP injection. Sidecar + adapter remain the cookie-delivery paths.
</ParamField>

<ParamField body="--skip-keychain-prompt" type="bool">
Sink: do not trigger the Chrome Safe Storage Keychain step during install. The sink daemon prompts on first sync instead.
</ParamField>

<ParamField body="--skip-partition-list" type="bool">
Sink: do not expand the Safe Storage partition list. Apple-tool callers may then prompt on first read.
</ParamField>

<ParamField body="--skip-keychain-access" type="bool">
Sink: do not run the legacy v0.10 set-keychain-access strategies (the kooky-CGO probe + partition/trust-list loop).
</ParamField>

<ParamField body="--skip-bridge-hint" type="bool">
Sink: suppress the post-install cookie-bridge env-var hint.
</ParamField>

`agentcookie wizard uninstall --as <role>` removes the LaunchAgent. Add `--purge` to also delete configs and paired keys.

## What `wizard install` writes

```text
~/.config/agentcookie/
  source.yaml          (source side; sink URL + Chrome DB path + peer hostname)
  sink.yaml            (sink side; listen addr + peer hostname + delivery + cdp)
  blocklist.yaml       (both sides; SQLite LIKE patterns, all commented by default)
  keys/<peer>.json     (per-peer X25519/HKDF-derived AES-256-GCM key)

~/.agentcookie/
  pairing.json         (source side, removed once pairing completes)
  logs/                (LaunchAgent stdout/stderr)
  cookies-plain.db     (sink side, plaintext sidecar; mode 0600)
  secrets/<cli>/       (sink side, secrets bus per CLI; mode 0600)
  chrome-profile/      (sink side, CDP-managed Chrome profile in degraded mode)

~/Library/LaunchAgents/
  dev.agentcookie.source.plist   (source; `agentcookie source --watch`)
  dev.agentcookie.sink.plist     (sink; `agentcookie sink`)
```

The installer uses `launchctl bootstrap gui/<uid> <plist>`; on an already-loaded label it falls back to `launchctl kickstart -k gui/<uid>/<label>` so re-running the wizard is idempotent.

## Troubleshooting first install

<AccordionGroup>
<Accordion title="detect Tailscale 100.x address for pair listener">
The wizard refuses to bind a pairing listener on `0.0.0.0`. Confirm `tailscale status` lists this machine and the peer, then re-run. Optional `--listen 127.0.0.1:9998` is accepted for local dev.
</Accordion>

<Accordion title="existing source.yaml/sink.yaml has peer.hostname X but --peer is Y">
`guardConfigPeerMismatch` refuses to leave a stale peer name in place because the running daemon would look up a key file that the next handshake saves under a different name. Re-run with `--force` (rewrites the config), or pass the original peer name.
</Accordion>

<Accordion title="existing paired key for <peer> found; skipping pairing">
Expected on a re-run. Pass `--repair` to force a fresh handshake (rotates the per-peer key).
</Accordion>

<Accordion title="universal keychain open did not complete">
Most often: the install has no TTY and no `AGENTCOOKIE_LOGIN_PASSWORD`. The wizard automatically downgrades to degraded (`skip_chrome_sqlite: true` + CDP) and continues. Upgrade later over SSH:

```bash
AGENTCOOKIE_LOGIN_PASSWORD='…' agentcookie wizard set-keychain-access
```
</Accordion>

<Accordion title="ssh sink 'agentcookie …' reports command not found">
`go install` placed the binary in `$HOME/go/bin`, which an SSH non-login shell may not have on `PATH`. The LaunchAgent uses absolute paths and is unaffected; for interactive use, add `~/go/bin` to the sink's shell profile or invoke `~/go/bin/agentcookie`.
</Accordion>
</AccordionGroup>

## Next

<CardGroup cols={2}>
<Card title="Quickstart" href="/quickstart">
Five-minute laptop + second-Mac pairing with verify-by-doctor steps.
</Card>
<Card title="Headless second-Mac install" href="/headless-install">
SSH-only install with the one-password Safe Storage open and the `wizard set-keychain-access` upgrade path.
</Card>
<Card title="Configure source and sink" href="/configure-source-sink">
Editing the written `source.yaml` / `sink.yaml`, listen-address validation, and CDP toggles.
</Card>
<Card title="LaunchAgent management" href="/launchagent-management">
The `dev.agentcookie.source` / `dev.agentcookie.sink` plists, log paths, and lifecycle.
</Card>
<Card title="Drive install from an agent" href="/agent-driven-install">
Using the bundled Claude Code skill to install on source + sink unattended.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Pairing `connection refused`, sink Keychain prompts, stale Chrome `SingletonLock`, and DBSC-suspect drops.
</Card>
</CardGroup>
