# Adopt a CLI with agentcookie.toml

> Authoring a v2 adoption manifest, declaring `[secrets]`, aliases, and `[[files]]`, running `agentcookie discover`, and migrating from imperative `secret import-from` to manifest-driven sync.

- 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/spec-agentcookie-secrets-bus-v2-adoption.md`
- `docs/runbook-adoption-manifest-author.md`
- `docs/runbook-secrets-bus-adoption.md`
- `examples/adoption-third-party-cli/agentcookie.toml`
- `examples/adoption-last30days/agentcookie.toml`
- `internal/secretsbus/manifest_v2.go`

---

---
title: "Adopt a CLI with agentcookie.toml"
description: "Authoring a v2 adoption manifest, declaring `[secrets]`, aliases, and `[[files]]`, running `agentcookie discover`, and migrating from imperative `secret import-from` to manifest-driven sync."
---

A v2 adoption manifest is a single TOML file named `agentcookie.toml` that declares a project's participation in the agentcookie secrets bus. The source machine scans well-known directories on startup, parses each manifest with `internal/secretsbus.ParseManifestV2`, applies the precedence and collision rules from spec section 4, and the next `agentcookie source` push reads the secrets in place and ships them to the sink — no `agentcookie secret import-from` call required. The schema, validator, and discovery loop are all v2-only; v1 imperative paths continue to work unchanged and surface in the registry as the `legacy-v1` tier.

## File location and discovery order

The manifest is always named `agentcookie.toml` (never `manifest.toml` — that name is taken by the v1 per-CLI sync override inside `~/.agentcookie/secrets/<name>/`). `Discover` walks the following roots in priority order and the first occurrence of a given `name` wins; lower-priority hits are recorded as soft-skipped.

| Priority | Path | Tier (`SourceKind`) |
|----------|------|---------------------|
| 1 | `~/.agentcookie/manifests/*.toml` | `explicit-manifest` |
| 2 | `~/.config/agentcookie/manifests/*.toml` | `explicit-manifest` |
| 3 | `/usr/local/share/agentcookie/manifests/*.toml` | `explicit-manifest` |
| 4 | `~/printing-press/library/*/.printing-press.json` | `pp-cli-derived` |
| 5 | User-added directories via `agentcookie discover` extra paths | `explicit-manifest` |
| 6 | `~/.agentcookie/secrets/<name>/` (v1 bus dirs) | `legacy-v1` |

<Info>
A malformed manifest is soft-skipped at discovery so a single bad file never blocks the loop. The imperative `agentcookie secret import-from` path stays strict and hard-fails on the same input.
</Info>

## Authoring the manifest

### Minimum schema

```toml agentcookie.toml
schema_version = 2                             # required; must be exactly 2
name = "my-tool"                               # required; slug rules below
display_name = "My Tool"                       # required; <= 200 printable UTF-8
description = "Internal tool"                  # optional; <= 200 chars
project_kind = "cli"                           # optional; cli|skill|service|other
homepage = "https://example.com/my-tool"       # optional

[secrets.file]
path = "~/.config/my-tool/.env"                # required when [secrets.file] is present
```

A manifest must declare exactly one `[secrets.*]` block **or** at least one `[[files]]` item. Two `[secrets.*]` blocks are a hard error (`at most one [secrets.*] block allowed`). The two reserved kinds, `[secrets.command]` and `[secrets.keychain]`, parse successfully but fail validation with `not yet supported in v2.0 (reserved for v2.1)`.

### Field reference

<ParamField body="schema_version" type="int" required>
Must be exactly `2`. `schema_version = 1` is rejected with a pointer to the v1 per-CLI sync override format. Future v3 manifests will be hard-rejected by v2-aware parsers.
</ParamField>

<ParamField body="name" type="string" required>
Lowercase ASCII letters, digits, and hyphens. 1–64 characters. Must start and end with a letter or digit. Pattern: `^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$|^[a-z0-9]$`. `..` substrings are rejected as a defense-in-depth traversal check. Identical validator as v1 `validCLIName`.
</ParamField>

<ParamField body="display_name" type="string" required>
Any printable UTF-8, 1–200 chars. Shown in `agentcookie discover` and `agentcookie secret list` headers. Never used as a path segment.
</ParamField>

<ParamField body="description" type="string">
One-line summary, ≤ 200 chars.
</ParamField>

<ParamField body="project_kind" type="enum">
One of `cli`, `skill`, `service`, `other`. Any other value is a hard parse error.
</ParamField>

<ParamField body="homepage" type="string">
Free-form URL; not validated beyond TOML parse.
</ParamField>

<ParamField body="signed_by" type="string">
Reserved for v2.1. Parsers accept the field and emit a `signed_by field is reserved for v2.1; ignored in v2.0` warning.
</ParamField>

<ParamField body="[secrets.file].path" type="string" required>
Path to the env-shaped file the agent reads on every push. `~/` expands to the user's home; absolute paths outside the home are accepted but warned as "may not be portable". `..` segments are a hard error. The file format is the v1 `secrets.env` dotenv subset.
</ParamField>

<ParamField body="[sync].default" type="bool" default="true">
Default-ship every key in the source file. Omitting the entire `[sync]` table is equivalent to `default = true`.
</ParamField>

<ParamField body="[sync.keys]" type="map[string]bool">
Per-key overrides. `KEY = false` excludes the key from the push envelope; `KEY = true` includes it when `default = false`. Applied source-side before envelope build; the sink never sees excluded keys.
</ParamField>

<ParamField body="[aliases]" type="map[string]string">
Declares `CONSUMER_ENV_VAR = "BUS_KEY"` mappings. `agentcookie secret env <name>` rewrites bus keys to consumer vars live on every call, so token rotation tracks automatically. Both sides must be valid env-var names (initial letter or underscore, then letters/digits/underscores). A local `agentcookie secret alias` for the same declared var overrides the manifest alias.
</ParamField>

### Sync filter

```toml
[sync]
default = true                                 # omitted => true

[sync.keys]
SETUP_COMPLETE   = false                       # exclude config-shaped keys
FROM_BROWSER     = false
INCLUDE_SOURCES  = false
```

Per-key entries always override `default`. Filter intent is source-side only; per v1 spec §4.3, the `[sync.keys]` table does not travel in the wire envelope.

### Aliases

```toml
[aliases]
TESLA_AUTH_TOKEN = "OAUTH_BEARER"              # CLI reads TESLA_AUTH_TOKEN; bus stores OAUTH_BEARER
```

A CLI that hardcodes `TESLA_AUTH_TOKEN` connects to a bearer agentcookie imported as `OAUTH_BEARER` for any user, with no per-user `agentcookie secret alias` call. An alias whose bus key is absent emits nothing for that declared var (no-op).

## Carried files (`[[files]]`)

`[[files]]` ships arbitrary file-shaped artifacts (multiline PEMs, TOML configs) that cannot ride as a single `KEY=VALUE` pair. It is a NEW construct that **coexists** with the single `[secrets.*]` block — declaring `[[files]]` does not count toward the "exactly one `[secrets.*]`" rule.

```toml
[[files]]
source   = "~/.config/tesla-pp-cli/config.toml"   # required; ~/ expands; no ".."
key      = "TESLA_CONFIG_TOML"                    # required; valid env-var name; unique per manifest
target   = "tesla-pp-cli/config.toml"             # required; relative, must stay inside ~/.agentcookie/
optional = false                                  # default false; true = opt-in (see below)
env      = "TESLA_CONFIG"                         # optional; absolute materialized path exported by `secret env`

[[files]]
source   = "~/.tesla/fleet-private.pem"
key      = "TESLA_FLEET_KEY_PEM"
target   = "tesla-pp-cli/fleet-key.pem"
optional = true
env      = "TESLA_FLEET_KEY_FILE"
```

### Carriage mechanism

Because the wire envelope is a flat `map[string]map[string]string`, each carried file adds two keys to its CLI's env map:

| Wire key | Value |
|----------|-------|
| `<key>` | Base64 encoding of the file bytes (single-line, dotenv-safe) |
| `_FILE_<key>` | The relative `target` — materialization instruction for the sink |

On the sink, the secrets writer decodes each `_FILE_<key>` / `<key>` pair, writes the decoded bytes mode `0600` to `<target>` resolved under `~/.agentcookie/`, and strips both keys from the per-CLI `secrets.env` so a carried file does not also leak as an env var. A sink that does not understand `_FILE_*` companions simply persists them as ordinary env keys (forward-degradation).

### Validation rules

- `source` is required, no `..` traversal, `~/` expands.
- `key` is required, must be a valid env-var name, must be unique across `[[files]]` entries (`[[files]] duplicate key "X"`).
- `target` is required, must be relative, must not contain `..`, must not resolve to an absolute path, and must stay strictly inside `~/.agentcookie/` after `filepath.Clean`.
- `env`, when set, must be a valid env-var name.
- Decoded payload size is capped at **256 KB** per file (`maxCarriedFileBytes`); larger payloads are refused by the sink rather than written.

### Opt-in items

An item with `optional = true` is **not** carried by default. The user enables it one-per-line in:

```
~/.agentcookie/file-optin/<name>.keys
```

Blank lines and `#` comments are ignored. This gates deliberate exposures (e.g. a command-signing key landing on a headless sink) behind explicit per-user consent.

<Warning>
Carried files are encrypted in transit and at rest in the bus, but the materialized file on the sink is a `0600` plaintext file owned by the sink user. On-sink protection is file permissions. Files are written only under `~/.agentcookie/`, never world- or group-readable, never outside that directory.
</Warning>

## Authoring workflow

<Steps>
<Step title="Copy a template">
Start from one of the in-repo examples:

```bash
cp examples/adoption-third-party-cli/agentcookie.toml ./agentcookie.toml
# or, for a dotenv-shaped skill:
cp examples/adoption-last30days/agentcookie.toml ./agentcookie.toml
```
</Step>

<Step title="Edit the four required fields">
Set `name` (slug), `display_name`, `[secrets.file].path`, and decide `[sync].default`. Add `[sync.keys]` to exclude config-shaped keys that are not real secrets.
</Step>

<Step title="Validate before shipping">
Either install locally and run the discover loop, or call the helper library from Go tooling:

<CodeGroup>
```bash CLI
mkdir -p ~/.agentcookie/manifests
install -m 0644 agentcookie.toml ~/.agentcookie/manifests/my-tool.toml
agentcookie discover --verbose
```

```go Go
import "github.com/mvanhorn/agentcookie/pkg/agentcookieadoption"

if err := agentcookieadoption.Validate(m); err != nil {
    log.Fatal(err)
}
_ = agentcookieadoption.WriteTo(m, "agentcookie.toml")
```
</CodeGroup>

`agentcookieadoption.Validate` applies the same rules as the in-tree parser at discovery time.
</Step>

<Step title="Ship the manifest from your installer">
Drop the file into `~/.agentcookie/manifests/` only when agentcookie is already present, so non-agentcookie users get a no-op:

```bash install.sh
if [ -d "$HOME/.agentcookie" ]; then
    mkdir -p "$HOME/.agentcookie/manifests"
    install -m 0644 agentcookie.toml "$HOME/.agentcookie/manifests/my-tool.toml"
fi
```
</Step>

<Step title="Push and verify on the sink">
```bash
agentcookie source --once
ssh other-mac "agentcookie secret list"
```

Your CLI's slug should appear with the expected key set.
</Step>
</Steps>

## `agentcookie discover`

`agentcookie discover` is the inspection command for the v2 registry. It does not push or modify anything; it only reads the well-known paths and prints the result.

```text
NAME           TIER               READ-IN-PLACE                       KEYS  COVERAGE
last30days     explicit-manifest  /Users/me/.config/last30days/.env   3     OK
my-tool        explicit-manifest  /Users/me/.config/my-tool/auth.env  0     OK
tesla-pp-cli   pp-cli-derived     /Users/me/.config/tesla-pp-cli/config.toml  1  OK
legacy-cli     legacy-v1          (legacy bus dir)                    2     OK
```

| Flag | Behavior |
|------|----------|
| `--verbose`, `-v` | Append a `skipped:` section listing every soft-skipped manifest with its reason, and a `discovery errors:` section for accumulated errors. |
| `--json` (root flag) | Emit `discoverJSONOutput { projects, skipped }` instead of the human table. `skipped` is only present with `--verbose`. |

The JSON shape per row:

<ResponseField name="name" type="string">Resolved slug after collision handling.</ResponseField>
<ResponseField name="kind" type="string">`explicit-manifest`, `pp-cli-derived`, or `legacy-v1`.</ResponseField>
<ResponseField name="source_path" type="string">Where the manifest came from (file path, or v1 bus subdirectory).</ResponseField>
<ResponseField name="read_in_place_path" type="string">Expanded `[secrets.file].path`; empty for `legacy-v1`.</ResponseField>
<ResponseField name="display_name" type="string">From the manifest; empty for legacy entries.</ResponseField>
<ResponseField name="key_count" type="int">Number of `[sync.keys]` overrides parsed.</ResponseField>
<ResponseField name="sync_summary" type="string">`default=<bool>, N overrides|allowed`.</ResponseField>
<ResponseField name="coverage" type="string">`OK` or `MISMATCH` when synced keys do not match the CLI's declared auth env var.</ResponseField>

## Collision rules

Two explicit manifests declaring the same `name` is a **hard error**: both are rejected from the registry and listed in `Skipped` with the collision message. Silent-skip would let an attacker shadow a real project by dropping a same-named manifest.

| Collision | Resolution |
|-----------|------------|
| Two explicit manifests, same `name` | Both rejected; collision message names both source paths. |
| Explicit vs. PP-CLI auto-derived | Explicit wins. The derived entry is renamed with a `-pp` suffix and both appear in the registry. |
| Two PP-CLI derivations, same `name` | Should be impossible (PP generator owns uniqueness). If it happens, first-by-path-sort wins; subsequent collisions get a 6-char sha256 suffix. |
| v1 bus dir vs. v2 manifest with same `name` | v2 wins for registry visibility; at push time, v1 bus values still win per-key over read-in-place values (preserving explicit user intent from a prior `import-from`). |

## Migrating from `agentcookie secret import-from`

The v1 imperative path remains supported:

```bash
agentcookie secret import-from ~/.config/my-tool/config.toml --as my-tool
```

It copies values into `~/.agentcookie/secrets/<name>/secrets.env`. Discovery recognizes that directory as a synthetic `legacy-v1` registry entry, so it continues to ship without further action.

<Tabs>
<Tab title="When to migrate">
- The project's file path is stable and the user wants live-rotation behavior.
- You author the CLI and can ship the manifest from your installer.
- You want zero per-machine, per-user setup.
</Tab>
<Tab title="When to stay imperative">
- The source file path is dynamic or computed at runtime.
- The user wants a stable snapshot rather than live values.
- The user wants to edit values in the bus without touching the project file.
</Tab>
</Tabs>

### Switchover with no re-login

1. Author and install `agentcookie.toml` pointing at the project's existing config file path.
2. Run `agentcookie discover`. The same slug should now appear under tier `explicit-manifest`. Per the collision rules, the v2 entry takes registry visibility while v1 bus values continue to win per-key — both data sources are honored simultaneously.
3. Push and verify on the sink: `agentcookie source --once` then `ssh sink "agentcookie secret list"`.
4. When confident the read-in-place flow is healthy, remove the old bus directory: `agentcookie secret rm <name>`. Discovery drops the `legacy-v1` entry on the next scan; the `explicit-manifest` entry remains.

<Tip>
Read-in-place means rotating a token in the project's own file ships on the next push automatically — no `agentcookie secret import-from` re-run, no manual sync command.
</Tip>

## Worked examples

<AccordionGroup>
<Accordion title="examples/adoption-last30days — skill reading a dotenv">

```toml
schema_version = 2
name = "last30days"
display_name = "last30days"
description = "Brand intelligence skill"
project_kind = "skill"
homepage = "https://github.com/mvanhorn/last30days-skill"

[secrets.file]
path = "~/.config/last30days/.env"

[sync]
default = true

[sync.keys]
SETUP_COMPLETE  = false
FROM_BROWSER    = false
INCLUDE_SOURCES = false
```

Ships every key in `.env` except three config-shaped ones the skill writes alongside its real secrets.
</Accordion>

<Accordion title="examples/adoption-third-party-cli — minimal CLI manifest">

```toml
schema_version = 2
name = "my-tool"
display_name = "My Tool"
description = "Example third-party CLI that ships secrets via agentcookie"
project_kind = "cli"

[secrets.file]
path = "~/.config/my-tool/auth.env"

[sync]
default = true
```

Smallest viable manifest: every key in the dotenv ships, no overrides.
</Accordion>

<Accordion title="Carried files alongside a dotenv">

```toml
schema_version = 2
name = "tesla-pp-cli"
display_name = "Tesla"
project_kind = "cli"

[secrets.file]
path = "~/.config/tesla-pp-cli/.env"

[aliases]
TESLA_AUTH_TOKEN = "OAUTH_BEARER"

[[files]]
source   = "~/.config/tesla-pp-cli/config.toml"
key      = "TESLA_CONFIG_TOML"
target   = "tesla-pp-cli/config.toml"
env      = "TESLA_CONFIG"

[[files]]
source   = "~/.tesla/fleet-private.pem"
key      = "TESLA_FLEET_KEY_PEM"
target   = "tesla-pp-cli/fleet-key.pem"
optional = true
env      = "TESLA_FLEET_KEY_FILE"
```

The dotenv rides via `[secrets.file]`; the `config.toml` always rides as a carried file; the PEM is opt-in and only ships when its `key` line appears in `~/.agentcookie/file-optin/tesla-pp-cli.keys`.
</Accordion>
</AccordionGroup>

## Edge cases

| Situation | Manifest answer |
|-----------|-----------------|
| Secrets live in macOS Keychain only | `[secrets.keychain]` is reserved for v2.1 and rejected today. Export keychain entries to a `~/.config/<tool>/.env` your tool also reads, point the manifest there. |
| Auth is a short-lived JWT minted per session | Bus is best for stable tokens. Ship the refresh token or OAuth `client_id`/`client_secret` instead and let each side mint its own JWT. |
| Secrets spread across multiple files | `[secrets.file]` is single-file. Either consolidate at install time, or ship two manifests with distinct slugs (`my-tool-auth`, `my-tool-config`). |
| File-shaped artifacts (PEMs, certs) | Use `[[files]]` (≤ 256 KB each) for portable artifacts, or keep machine-bound material outside the bus entirely. |
| Multiple installs of one tool (dev + prod) | Bus is single-account. Give each install a distinct slug and ship two manifests. |

## What not to do

- **Don't put real secrets in the manifest.** It is a *pointer* to where secrets live; the secrets stay in the file `[secrets.file].path` references.
- **Don't ship the manifest from an untrusted source.** `[secrets.file].path` is a path-traversal vector that the parser validates, but `curl | bash` from a public CDN bypasses your usual trust boundary.
- **Don't sign the manifest yet.** v2.0 ignores `signed_by` with a stderr warning; v2.1 will define the verification flow.
- **Don't declare a second `[secrets.*]` block.** That's a hard error. Use `[[files]]` for additional non-env-shaped artifacts.
- **Don't rename `agentcookie.toml` to `manifest.toml`.** The bare name `manifest.toml` is structurally different and already used by v1 per-CLI sync overrides inside `~/.agentcookie/secrets/<name>/`.

## Next

<CardGroup cols={2}>
<Card title="agentcookie.toml manifest reference" href="/manifest-v2-reference">
Full schema, validation rules, reserved fields, and the three integration tiers in one place.
</Card>
<Card title="Secrets bus on-disk layout" href="/secrets-bus-layout">
Where the sink writes `secrets.env`, sealed twins, and materialized `[[files]]` payloads.
</Card>
<Card title="Secrets bus overview" href="/secrets-bus">
How the bus rides the same encrypted push as cookies and the v2 adoption tiers.
</Card>
<Card title="pkg/agentcookiesecret reader library" href="/go-reader-library">
In-process Go API for consuming the bus from a CLI instead of shelling out.
</Card>
</CardGroup>
