feat(secrets): SC-6 encrypted secrets + ${SECRET:NAME} substitution #636
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!636
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "boss/628"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Encrypted-secrets store (SC-6 / closes #628) — adds the AES-256-GCM
secrettable, the${SECRET:NAME}substitution helper, the operator-gated/secretsHTTP surface, the/settings/secretsdashboard, and thesecret_access_logaudit trail.CLAUDE_HOOKS_SECRET_KEYis missing or decodes to fewer than 32 bytes (matchesOAUTH_ENCRYPTION_KEYvalidation pattern). Documented indocs/credentials.md.decrypt(row, accessor, reason)always writes onesecret_access_logrow before returning the plaintext; the substitution helper de-dupes audit rows per name per render.${SECRET:NAME}placeholders that reference an unknown name throwMissingSecretError— partial substitution is never returned.guardMutating;GET /secretsreturns names + metadata only, never bodies.<Drawer>,<Button>,<ConfirmDialog>perapps/web/CLAUDE.md; values are write-only (input always reset to empty on edit).Client-facing migration introduces the
secret+secret_access_logtables idempotently viaensureSchema. The substitution helper is ready foragent-env-sync.renderForInstanceto call (SC-2 wires the consumer).Test plan
bun test src/infrastructure/secrets.test.ts src/http/handlers/secrets.test.ts— 39 specs covering boot validation, AES-256-GCM round-trip, tamper rejection,MissingSecretError, audit-log emission + cascade, HTTP CRUD, rotate, paginated access log, name-shape enforcement.just qa— typecheck + Biome lint + format clean.POST /secrets→GET /secrets(no body) →POST /secrets/:name/rotate→GET /secrets/:name/access-log.Closes #628
Adds the AES-256-GCM secret store, the `${SECRET:NAME}` substitution helper, the `/secrets` HTTP surface, the `/settings/secrets` dashboard, and `secret_access_log` audit trail. Boot fails when `CLAUDE_HOOKS_SECRET_KEY` is missing or decodes to fewer than 32 bytes. Closes #628 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>All ACs from #628 met. Crypto correct (AES-256-GCM, 12-byte IV, 16-byte auth tag, fresh per row). Audit log written on successful decrypt only. All 6 HTTP routes gated via
guardMutating. Dashboard reuses<Drawer>,<Button>,<ConfirmDialog>. Test coverage solid: round-trip, tamper, missing-secret, substitution, deep walk.Nits (not blocking):
secrets.ts:500comment "The audit log captures the access intent regardless" is wrong — ifdecryptthrows, no audit row is written. Remove or invert the sentence.handleSecretsReplacecoercesdescriptiondifferently fromhandleSecretsCreate(empty string →""vsnull). The UI never sends""so this is harmless, but clearing a description via the edit drawer silently preserves the old value instead of clearing it. Considerdescription || nullinstead of the bare string pass-through.renderForInstancewiring deferred to SC-2/SC-5 as expected — the helpers are exported and ready.861b67c4f6a98107bde5