Cost control & secret/extension security¶
Three native ports of things paperclip does well, into the Leopold harness: a USD
budget hard-stop, a secret vault that keeps secrets out of the prompt, and
capability-gating for extensions. No Postgres, no daemon — state in .leopold/,
secrets encrypted on disk, consent at the CLI.
1. USD budget hard-stop (SDK driver)¶
The Claude Code CLI already reports total_cost_usd per session, so there is no model
price map: the driver accumulates the real cost per item and stops the run when it
crosses the cap. This is the dependable dollar ceiling for an autonomous run.
leopold-driver run --budget-usd 5(orLEOPOLD_BUDGET_USD=5) sets a $5 cap.worker.tsreadstotal_cost_usdfrom theresultevent;loop.tsaccumulates it intostate.spent_usdand logs acostevent per item.- Enforced at the top of the loop (before each item): once
spent_usd >= budget_usdthe run stops withbudget_exceeded, notifies, and leaves the work staged. - Pure decision functions (
parseBudgetUsd,overBudget) inbudget.tsare unit-tested.
2. Secrets out of the prompt (encrypted vault)¶
Today the worker is a raw Claude Code session, so any secret the work needs tends to get
typed into the prompt/transcript. This injects secrets as environment variables
instead: they reach the worker's Bash tool as $NAME but never enter the prompt.
leopold-driver secrets set NAME(value read from stdin, so it never lands in shell history) encrypts into.leopold/secrets.env;secrets listshows names only.- At rest: AES-256-GCM. The 32-byte master key lives at
~/.claude/leopold/secrets.key(mode0600, generated on demand); the vault is the encrypted blob. Wrong/rotated key → fail closed (no secrets). worker.tsdecrypts the vault and sets the values intoprocess.envfor the item (and passes them asoptions.env), restoring the environment afterward. The worker is told to use$NAMEand never echo a value.- Protection is encryption at rest, not a read guard: the vault is an AES-256-GCM
blob and the master key is
0600outside the project, so readingsecrets.envyields ciphertext with no key. The worker never needs the file — it gets the values as$NAME.
3. Capability-gating for extensions¶
An extension declares what it does up front, and the toolchain menu requires consent before granting it on install/update.
extension.jsongains acapabilitiesarray, e.g. ovmem:["network", "settings.write", "filesystem.home", "package.install", "process.spawn"].leopold-menu.shshows the declared capabilities in the component view, andInstall/Updatenow route throughext_consent— it prints the capabilities and requires aybefore runningmanage.sh install/update. No declaration → nothing to gate. All four bundled extensions (leopold, serena, gstack, ovmem) declare theirs.
Files¶
| Area | Files |
|---|---|
| Budget | packages/driver/src/{budget,config,loop,worker,types,index}.ts, test/budget.test.ts |
| Secrets | packages/driver/src/{secrets,worker,guard,index}.ts, hooks/guard-irreversible.sh, test/secrets.test.ts |
| Capabilities | extensions/*/extension.json, scripts/leopold-menu.sh |
Verification¶
make driver-test(unit): budget decisions; secret round-trip + on-disk encryption (no plaintext) +0600key + env apply/restore; the guard suite (git commit/push lock only).- CLI smoke:
secrets setvia stdin encrypts (no plaintext in the vault),secrets list, key is0600, invalid names rejected.leopold menushows capabilities and gates install/update on consent. tsc --noEmitandmake hooks-checkgreen.
Note: budget and secret injection target the SDK driver (Path A), where the driver controls the worker. The bash guard still protects the secret files in the in-session path.