Skip to content

bob secrets

bob secrets — Presence Check for Declared Secrets

Section titled “bob secrets — Presence Check for Declared Secrets”

A client’s BoB can confirm that every secret it needs is in place without anyone reading, copying or transmitting the secret (#2286, part of #2215). (Back to reference index.)

The operator profile (operator.md) declares the secrets an operator’s BoB depends on as names and sources — never a value. bob secrets check reads that list, probes each entry for presence on this machine, and names the declared source for anything missing. It turns the “verify presence, name the source of truth” rule from the tailscale-remote-ops and secrets-management skills into one shared, tested implementation. Storing, distributing and rotating stay with the declared source (1Password, gh secret set, scripts/fleet/distribute-secret.sh); this command only ever answers is it there?

Terminal window
bob secrets check # every declared secret: state, source, fix
bob secrets check --json # one bob-secrets/1 document
bob secrets check --operator <dir> # probe that operator repo instead of the resolved one
node scripts/security/secrets.js probe <kind> <ref> [--name <SECRET_NAME>] [--json]
Option Effect
--json Print one bob-secrets/1 document instead of the table.
--operator <dir> Use this operator repo for the run — the same as BOB_OPERATOR=<dir> — instead of the resolver’s answer ($BOB_OPERATOR → $BOB_HOME/.bob-operator → ~/projects/bob-operator).
probe <kind> <ref> One probe for a caller that already holds the locator. --name sets the secret name a gh-secret ref is matched against. Exit 0 present · 1 missing or insecure · 3 unknown · 2 usage.
Exit Meaning
0 Every required secret is present. An unrequired gap is reported, never graded.
1 A required secret is missing, insecure or unknown.
2 The operator profile is invalid (missing, unparseable, or failing schemas/operator.schema.json), or usage.
3 No operator resolved — bob operator init <dir> --link, or export BOB_OPERATOR=<dir>.

The document is printed whole on every exit; with --json an error is an error object (code, message, fix) on the same document, never a bare message.

Declaring where a secret lives — present_at

Section titled “Declaring where a secret lives — present_at”

Each secrets[] entry in operator.json may carry a presence locator:

{
"name": "BOB_CHECKOUT_TOKEN",
"source": "gh-secret example-co/*:BOB_CHECKOUT_TOKEN",
"required": true,
"present_at": { "kind": "gh-secret", "ref": "example-co/example-app" }
}

source stays what it was — where the value is kept, in the credential inventory’s reference notation — and present_at says where this machine should find it. kind is a closed set and ref is the locator for that kind; present_at refuses any other key, so the locator can never hold a value either. required: true makes the entry grade the exit code; absent or false, the entry is reported but never fails the run, so an operator can adopt locators one at a time without going red. An entry with no present_at is reported unknown with a “declare present_at” fix.

kind ref What it names
env a variable name (BRIDGE_SECRET) an environment variable of the shell that runs the check
file a path; ~ expands (~/.claude/secrets/reporter-links.md) a regular file on this machine
keychain service or service/account (example-co-signing/ada) a macOS login-keychain generic password
gh-secret <owner>/<repo> or org:<org>, optionally :<NAME> when the forge secret is not named like the entry (example-co/app, org:example-co:CI_TOKEN) an Actions secret on the forge
op Vault/Item or op://Vault/Item[/field] (Example/remote-decision-bridge) a 1Password item; a field suffix is ignored, only the item is probed

Both fixtures beside the schema prove it both ways in make check-schemas: operator.sample.json declares every kind and validates; operator.present-at.rejected.json (a kind outside the set, carrying a value) must fail.

State Meaning Fix printed
present the locator resolves to something that exists —
missing the locator resolves cleanly to nothing how to place it from its source: export NAME=…, place the file then chmod 600, security add-generic-password, gh secret set (or distribute-secret.sh distribute for BOB_CHECKOUT_TOKEN), op item create
insecure a file exists but is readable by group or others chmod 600 <path>
unknown it could not be probed: no present_at, an unparseable ref, op or gh not on PATH or not signed in, a keychain entry on a non-macOS host, a locked keychain install / sign in / declare the locator; for keychain, run the check on the Mac that holds it

unknown is never reported as present. A required secret that is unknown is a gap — exit 1 — because the machine cannot show it is there.

How each kind is probed — existence and metadata only

Section titled “How each kind is probed — existence and metadata only”
kind Probe What comes back
env is the variable set and non-empty a boolean; the value is never copied
file stat — type and mode exists, regular file, mode & 077
keychain security find-generic-password -s <service> [-a <account>] without -w; stdout discarded exit 0 present · 44 missing · other unknown
gh-secret gh secret list -R <owner>/<repo> or --org <org>; the first column of every row whether the name is listed; gh exit 4 is “not authenticated”
op op item list --format json [--vault <vault>]; item title and id only — never op item get, op read or --fields whether the item is listed

Every subprocess runs with stderr discarded and its stdout consumed by the probe that asked (a listing of names); neither is copied into a detail line, a fix or an error message. The only strings a report carries are the profile’s own names and refs, the engine’s fixed vocabulary and exit codes. That is why this command runs at automation Level 3 without widening the autonomous profile’s deny list on credential reads.

{
"schema": "bob-secrets/1",
"generated_at": "2026-09-16T05:40:00.000Z",
"host": "farm-01",
"platform": "linux",
"operator": { "resolved": true, "source": "pointer", "root": "/home/me/projects/bob-operator", "id": "example-co" },
"ok": false,
"verdict": "missing",
"counts": { "declared": 5, "present": 3, "missing": 1, "insecure": 0, "unknown": 1, "required": 2, "required_gaps": 1 },
"missing_required": ["BOB_CHECKOUT_TOKEN"],
"secrets": [
{ "name": "BOB_CHECKOUT_TOKEN", "source": "gh-secret example-co/*:BOB_CHECKOUT_TOKEN", "required": true, "kind": "gh-secret", "ref": "example-co/example-app", "state": "unknown", "detail": "cannot list secrets on repo example-co/example-app: gh is not authenticated", "fix": "gh auth login" }
],
"error": null
}

verdict is ok · missing (a required gap) · invalid · unresolved. counts are the document’s own; missing_required lists the names of required secrets whose state is not present. An entry keeps inventory_id when the profile declares one.

bob doctor (host-drift.js) carries the summary of this check on its bob-host-drift/1 document as secrets — the verdict, the counts by state, how many are required and the names of required secrets not present — and prints it as one line after the operator line, never graded:

INFO secrets: 5 declared · 5 present · 0 missing · 0 insecure · 0 unknown — every required secret (2) present (#2286)
WARN secrets: 5 declared · 3 present · 1 missing · 0 insecure · 1 unknown — 1 required not present: BOB_CHECKOUT_TOKEN — Fix: bob secrets check (#2286)
INFO secrets: no operator declared — nothing to verify (#2286)
WARN secrets: operator profile invalid — … — Fix: bob operator show (#2286)

A gap is the operator’s todo, not BOB_HOME drift, so the machine verdict is unchanged. The fleet snapshot’s summary.host_drift.secrets and bob status --scope host carry the compact form (verdict, counts, missing_required), so a support health report holds presence and names and never a value — the summary has no per-secret rows at all.

scripts/fleet/distribute-secret.sh status asks the same gh-secret probe (secrets.js probe gh-secret <slug> --name BOB_CHECKOUT_TOKEN) instead of its own gh secret list | grep, so the fleet helper and bob secrets check ask the forge the same question the same way. Its rows, summary line and exit codes are unchanged; a gh that cannot list still reads as “secret MISSING”, as the old grep did.

secrets-inventory.json is the product-wide declaration of every credential and every store a copy lives in; cred-check.js compares the inventory to the stores by name. The operator profile is the per-operator list of the secrets its BoB needs, and bob secrets check asks whether each one is reachable from this machine. The two agree through inventory_id on a secrets[] entry; neither ever holds a value.

tests/test-secrets.js proves the parser per kind, every probe state with an injected exec and stub CLIs on a sealed PATH (no real gh, op or security can be found), the document and its exit codes, the CLI flags and the bin/bob route — and the canary: a value planted behind every kind (the env variable, the file’s content, a security stub, a gh stub on stdout and stderr, an op stub’s fields) must appear in no stdout, stderr, --json document or file the command writes. tests/test-host-drift.js proves the doctor line and the compact snapshot form; tests/test-distribute-secret.js proves distribute-secret.sh status on the shared probe with its wording and exit codes preserved.