# Secrets bus on-disk layout

> The v1 standard layout under `~/.agentcookie/secrets/<cli>/`, file modes (0600), `secrets.env` format, optional sealed twin, and the `[[files]]` materialization path.

- 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-v1.md`
- `internal/secretsbus/secretsbus.go`
- `internal/secretsbus/writer.go`
- `internal/secretsbus/filecarriage.go`
- `internal/sinkpush/seal.go`

---

---
title: "Secrets bus on-disk layout"
description: "The v1 standard layout under `~/.agentcookie/secrets/<cli>/`, file modes (0600), `secrets.env` format, optional sealed twin, and the `[[files]]` materialization path."
---

The secrets bus is a per-CLI on-disk contract rooted at `~/.agentcookie/secrets/`. The sink (`internal/secretsbus/writer.go`) materializes everything the source ships: one mode-`0700` directory per CLI containing an optional `manifest.toml`, a plaintext `secrets.env` (`0600`), an opaque `secrets.env.sealed` twin when sealing is on, plus any carried files written under `~/.agentcookie/<target>` via the `[[files]]` companion-key path. Any tool that speaks the v1 dotenv grammar can consume the directory directly; the format is the public boundary, not the Go reader.

## Directory layout

```
~/.agentcookie/                         # 0700
├── secrets/                            # 0700, root of the bus
│   ├── <cli-name>/                     # 0700, one per consumer
│   │   ├── manifest.toml               # 0600, v1 metadata + sync policy
│   │   ├── secrets.env                 # 0600, plaintext KEY=VALUE lines
│   │   └── secrets.env.sealed          # 0600, optional sealed twin
│   └── …
├── file-optin/                         # opt-in gate for [[files]]
│   └── <cli-name>.keys                 # one carry-key per line
└── <cli-name>/                         # 0700, [[files]] materialization root
    └── <file-target>                   # 0600, e.g. config.toml, fleet.pem
```

`<cli-name>` rules are enforced symmetrically by `validCLIName` in `internal/secretsbus/secretsbus.go` and by the sink writer before any path is touched:

<ResponseField name="character set" type="string">Lowercase ASCII letters, digits, and hyphen only.</ResponseField>
<ResponseField name="boundaries" type="string">May not begin or end with a hyphen.</ResponseField>
<ResponseField name="length" type="int">1 to 64 characters inclusive.</ResponseField>
<ResponseField name="rejected tokens" type="string">`.`, `..`, slashes, backslashes, whitespace, any other punctuation.</ResponseField>

<Warning>
Both source and sink refuse a payload whose name fails these checks and write nothing outside the secrets root. This is the path-traversal defense; readers must not normalize names silently.
</Warning>

## File modes and atomic writes

Every file the writer creates is `0600`, every directory is `0700`. Writes go through `atomicWrite` in `internal/secretsbus/writer.go`:

1. Open a sibling temp file `<path>.tmp` with `O_WRONLY|O_CREATE|O_TRUNC` at `0600`.
2. Write payload bytes, then `fsync`.
3. `Close`, then `os.Rename(tmp, path)`.
4. On any failure, remove the temp and surface the error.

A reader following the section 5.2 priority chain (sealed first, plaintext fallback) therefore observes the previous value or the new value, never a torn read. The bus assumes a single writer per directory at a time — the sink during `/sync` and `agentcookie secret set` invoked manually — coordinated only by atomic rename; there is no lock file.

## `secrets.env` format

`secrets.env` is UTF-8, line-oriented, and parsed by `parseEnvFile` against a strict dotenv subset. The sink writes the canonical render via `renderEnvFile` and prepends two informational comments:

```text
# Written by agentcookie sink. See docs/spec-agentcookie-secrets-bus-v1.md for format.
# Do not hand-edit while a sync is in progress: the next sync overwrites this file.
EXAMPLE_API_KEY=ex_live_3f4a2c91d8e6b5a07c1f9e4b6d0a8c2e
TESLA_CONFIG=/Users/you/.agentcookie/tesla-pp-cli/config.toml
```

### Grammar

| Construct | Form | Rule |
| --- | --- | --- |
| Comment | `# …` | Whole-line only. No trailing comments. |
| Blank | empty line | Ignored. |
| Entry | `KEY=VALUE` | `=` flush against key and value. No whitespace around `=`. |
| Key | `[A-Za-z_][A-Za-z0-9_]*` | Case-sensitive. Hyphens and dots are invalid in keys. |
| Bare value | printable, no whitespace, no `#`, `=`, `\`, `"` | Reader returns it verbatim. |
| Double-quoted | `"…"` | Escapes `\"`, `\\`, `\n`, `\r`, `\t`. |
| Single-quoted | `'…'` | Verbatim — no escapes recognized. |
| Continuation | trailing `\` | Joins next line; backslash and newline are dropped. |

<Note>
The 256 KiB cap (`maxEnvFileBytes`) is enforced before parse on the source. Larger files are skipped and surfaced as non-fatal errors; other CLIs in the same `LoadPayload` walk still ship.
</Note>

### Explicit forbiddens

The reader rejects these dotenv-family extensions outright:

- Variable interpolation (`$OTHER`, `${OTHER}`) — value is taken literally.
- Command substitution (`$(cmd)`).
- `export KEY=…` prefix.
- Heredoc / triple-quoted blocks.
- Whitespace around the `=` sign.

`parseEnvFile` returns an error citing the offending line number; conforming readers must not skip a bad line silently because the next-priority source would then leak in.

### Reserved key prefixes

| Prefix | Meaning | Behavior |
| --- | --- | --- |
| `_unknown_<field>` | Field that `secret import-from` could not map to a canonical key. | Surfaced as an ordinary entry so the user can rename. |
| `_BIN_<KEY>` | Original value was raw bytes. | Stored as base64. Consumers that recognize the prefix may decode. |
| `_meta_<field>` | Reserved for future metadata. | Reader ignores or passes through. |
| `_FILE_<K>` | Carried-file companion holding the materialization target. | Consumed by the sink; never lands in `secrets.env`. |
| `_FILEENV_<K>` | Carried-file companion holding an env-var name. | Consumed by the sink; never lands in `secrets.env`. |
| `__…` | Reserved double-underscore namespace. | Writers must not emit; readers ignore silently. |

## `manifest.toml`

The v1 manifest lives at `~/.agentcookie/secrets/<cli>/manifest.toml` and describes the per-CLI dataset together with its sync policy. The Go type and loader live in `secretsbus.go`:

```toml
schema_version = 1
display_name = "Example PP CLI"

[sync]
default = true

[sync.keys]
EXAMPLE_OAUTH_REFRESH = false
_BIN_EXAMPLE_SIGNING_PRIVATE_KEY = false
```

<ParamField path="schema_version" type="int" required>Always `1` for the format described here. Higher versions surface a warning; the reader may return `error` or best-effort.</ParamField>
<ParamField path="display_name" type="string" required>UTF-8, 1–128 characters. Used in `agentcookie secret list` and doctor output.</ParamField>
<ParamField path="sync.default" type="bool" default="true">When omitted entirely, the loader applies `true`. `loadManifest` uses a two-pass decode to distinguish explicit `false` from the absent case.</ParamField>
<ParamField path="sync.keys" type="map<string,bool>">Per-key override. `true` always ships, `false` never ships, regardless of `[sync] default`. Keys absent here inherit `default`.</ParamField>

`applySyncPolicy` walks the env map in sorted order so the resulting wire payload is deterministic. The `[sync.keys]` table never travels; only the filtered key/value set does. The sink therefore never sees a key the source's policy already dropped.

## `secrets.env.sealed` — the optional twin

When the source pushes with sealing requested AND a v0.12 master key is available in the Keychain, `WritePayload` writes a sealed twin in addition to the plaintext file:

```go
if masterKey != nil {
    sealed, err := keystore.Seal(masterKey, envBytes)
    // …
    sealedPath := filepath.Join(cliDir, "secrets.env.sealed")
    atomicWrite(sealedPath, sealed, 0o600)
}
```

The sealed body is opaque. Adapters that bake sealed values into per-CLI config files share the same envelope: `internal/sinkpush/seal.go` advertises `SealedPrefix = "agc1:"` and `maybeSeal()` produces `"agc1:" + base64(seal(masterKey, plaintext))`, transparently degrading to plaintext when the master key is absent. For the bus twin itself, only the filename and the resolution order are part of the public contract.

### Resolution order

A reader that understands sealing checks the per-CLI directory in this order:

<Steps>
<Step title="`secrets.env.sealed`">
If present, decrypt via the agentcookie reader library or `agentcookie secret get`. On success the resulting map is the dataset; the plaintext sibling, if it exists, is ignored.
</Step>
<Step title="`secrets.env`">
Used only when the sealed twin is absent. Parsed under the v1 grammar above.
</Step>
<Step title="Caller-registered fallback file">
Optional second argument naming the consumer's pre-existing config file. Keys not already provided by the bus may be filled in via a reader-defined heuristic mapping.
</Step>
<Step title="Process environment">
Last-resort source so adopting the bus does not break existing setups. **Bus wins over env** — this is the single most important non-obvious rule in the spec.
</Step>
</Steps>

<Warning>
A reader that finds an unreachable sealed file (sealing on but master key unavailable) MUST return an error rather than fall back to plaintext on a sealed machine. The plaintext sibling on a sealed machine is normally absent, and silent fallback would mask a real misconfiguration.
</Warning>

Sealing policy in `WritePayload` resolves three cases:

| `sealingEnabled` | Master key present | Result |
| --- | --- | --- |
| `false` | — | Plaintext only. |
| `true` | yes | Plaintext + sealed twin in the same atomic-rename window. |
| `true` | no | Plaintext only, with a non-fatal error so `/sync` still succeeds. |

## `[[files]]` materialization

A v2 adoption manifest may declare `[[files]]` items that carry arbitrary files — multiline PEMs, TOML configs — from source to sink and materialize them under `~/.agentcookie/<target>` at mode `0600`. Because the wire envelope is `map[string]map[string]string`, each item rides as two reserved companion keys plus an optional env-name pointer.

### Manifest item

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

<ParamField path="source" type="string" required>Absolute path or `~/`-prefixed path on the source machine. Must exist, be a file, and stay under 256 KiB.</ParamField>
<ParamField path="key" type="string" required>Wire envelope key under which the base64-encoded payload travels. Must be a valid env-var name; must be unique across `[[files]]`.</ParamField>
<ParamField path="target" type="string" required>Relative path under `~/.agentcookie/` on the sink. No `..`, no absolute paths, no escape from the root.</ParamField>
<ParamField path="env" type="string">Env-var name the sink emits via `secret env`, pointing at the ABSOLUTE materialized path of the carried file. Lets a path-reading CLI consume the synced file without hardcoding a per-machine path.</ParamField>
<ParamField path="optional" type="bool" default="false">When `true`, discovery does NOT carry the item unless the user opts in via `~/.agentcookie/file-optin/<cli>.keys`.</ParamField>

### On-wire shape

`CarryFiles` (`internal/secretsbus/filecarriage.go`) produces three keys per item in the CLI's flat env map:

```text
<key>                = <base64(file bytes)>
_FILE_<key>          = <target relative path>
_FILEENV_<key>       = <env var name>       # only when item.Env is set
```

`CarryFileKey()` and `CarryFileEnvKey()` are the helpers that produce those reserved names. The opt-in gate lives at `~/.agentcookie/file-optin/<cli>.keys`, one key per line; `#` comments and blank lines are ignored. A missing file means "nothing opted in," so an `optional = true` item is silent until the user adds its `key` there. Default (non-optional) items skip this gate entirely.

### Sink materialization

`MaterializeFiles` runs before `secrets.env` is rendered, so a carried file never also leaks as an env-var payload:

```text
sink: WritePayload(homeDir, payload, sealing)
        for each cli in payload:
            safe := drop invalid key names
            res, consumed, errs := MaterializeFiles(homeDir, cli, safe)
            delete consumed keys from safe
            for envName, absPath := range res.EnvAdditions:
                safe[envName] = absPath         # points e.g. TESLA_CONFIG at the file
            atomicWrite(secrets.env, render(safe), 0600)
            if masterKey != nil: atomicWrite(secrets.env.sealed, seal(...), 0600)
```

Each `_FILE_<K>` companion triggers:

1. Re-validate `<target>` with `validateMaterializeTarget` (relative, no `..`, no absolute, no root escape).
2. Base64-decode the payload under `<K>`.
3. Refuse if decoded bytes exceed 256 KiB.
4. `safeJoinUnderRoot(~/.agentcookie, target)` — rejects anything outside the agentcookie root after `filepath.Clean`.
5. `os.MkdirAll(dir, 0700)`, then `atomicWrite(dest, decoded, 0600)`.
6. Mark `<K>`, `_FILE_<K>`, and (when present) `_FILEENV_<K>` consumed so they never reach `secrets.env`.

If an `env` companion is set and names a valid env-var, `MaterializeResult.EnvAdditions[envName] = absPath` is merged into the CLI's env map before render, so `secret env` emits e.g. `TESLA_CONFIG=/Users/you/.agentcookie/tesla-pp-cli/config.toml`.

<Tip>
The sink does not trust the wire. Every target is re-validated even though the source's `ParseManifestV2` already validated the manifest. A hand-built payload that bypasses parse cannot escape `~/.agentcookie/`.
</Tip>

### Counters

`WriteResult` reports the per-`/sync` outcome the sink uses for logging:

<ResponseField name="CLIsWritten" type="int">CLIs that received at least one file write.</ResponseField>
<ResponseField name="KeysWritten" type="int">Total KEY=VALUE pairs persisted across all CLIs.</ResponseField>
<ResponseField name="SealedWritten" type="int">Number of `secrets.env.sealed` files written.</ResponseField>
<ResponseField name="PlaintextWritten" type="int">Number of `secrets.env` files written.</ResponseField>
<ResponseField name="FilesMaterialized" type="int">Number of carried files written under `~/.agentcookie/`.</ResponseField>

## Defensive validation surface

| Layer | Check | Failure mode |
| --- | --- | --- |
| Source `LoadPayload` | `validCLIName(<dir>)` | Skip dir, append non-fatal error. |
| Source `LoadPayload` | `secrets.env` size ≤ 256 KiB | Skip CLI, ship others. |
| Source `parseEnvFile` | Whitespace around `=`, invalid key | Return error with line number. |
| Source `applySyncPolicy` | `[sync.keys]` overrides | Per-key drop before wire. |
| Source `CarryFiles` | Source exists, not a directory, size ≤ 256 KiB | Item dropped, error surfaced. |
| Sink `WritePayload` | `validCLIName(<from wire>)` | Refuse to write that CLI. |
| Sink `WritePayload` | `validKeyName(<each k>)` | Drop key, log error. |
| Sink `MaterializeFiles` | `validateMaterializeTarget` + `safeJoinUnderRoot` | Refuse, write nothing. |
| Sink `MaterializeFiles` | Decoded size ≤ 256 KiB | Refuse, log error. |
| Sink `MaterializeFiles` | Strip dangling `_FILEENV_*` | Companion never lands in `secrets.env`. |

## Worked example

After a successful `/sync` for a Tesla PP CLI that carries an OAuth bearer and a fleet key file:

```text
~/.agentcookie/
├── secrets/
│   └── tesla-pp-cli/
│       ├── manifest.toml          0600  schema_version = 1, [sync] default = true
│       ├── secrets.env            0600  TESLA_OAUTH_BEARER=…
│       │                                TESLA_CONFIG=/Users/you/.agentcookie/tesla-pp-cli/config.toml
│       │                                TESLA_FLEET_KEY_FILE=/Users/you/.agentcookie/tesla-pp-cli/fleet.pem
│       └── secrets.env.sealed     0600  agc1-sealed bytes (when master key present)
└── tesla-pp-cli/
    ├── config.toml                0600  carried from source ~/.config/tesla-pp-cli/config.toml
    └── fleet.pem                  0600  carried from source PEM
```

The plaintext `secrets.env` contains only the OAuth bearer plus the two pointer env vars; the raw base64 payload key and `_FILE_*` / `_FILEENV_*` companions were consumed during materialization and never reach disk.

## Related pages

<CardGroup cols={2}>
<Card title="Secrets bus" href="/secrets-bus">The end-to-end push: how bearer tokens, API keys, and KEY=VALUE blobs ride the same encrypted transport into this layout.</Card>
<Card title="Manifest v2 reference" href="/manifest-v2-reference">Full schema for `agentcookie.toml`, including `[secrets]`, `[aliases]`, and `[[files]]`.</Card>
<Card title="pkg/agentcookiesecret reader" href="/go-reader-library">In-process Go API that applies the sealed → plaintext → fallback → env priority chain for a consuming CLI.</Card>
<Card title="Wire protocol v1" href="/wire-protocol">`POST /sync`, `SyncEnvelope` JSON fields, and AES-256-GCM seal that wraps the bus payload.</Card>
<Card title="Adopt a CLI with agentcookie.toml" href="/adopt-a-cli-manifest">Authoring `[secrets]` plus `[[files]]` for a new consumer and running `agentcookie discover`.</Card>
<Card title="Configuration files reference" href="/config-files-reference">Schemas and validation rules for the surrounding `source.yaml`, `sink.yaml`, allow/block lists.</Card>
</CardGroup>
