Skip to content

Ergonomics: provisioning a headless signing agent is a ~9-step ceremony — make it one command #382

Closed

Description

@bordumb

Context

Setting up a delegated agent that can sign commits/releases headlessly (no fingerprint, for CI or an autonomous assistant) is the product's flagship use case — "one budget, N agents, provable" applies just as much to signing as to spending. But provisioning one today is a long, undocumented ceremony that mixes CLI commands with manual git surgery on the registry. This session got there and it verified end to end (agent-signed commit accepted through the pinned root, PR #381), but only after ~9 steps and discovering several bugs along the way.

This issue is purely about ergonomics: collapse the ceremony into a single command. (The correctness bugs found en route are fixed separately in #381; residual bugs are listed at the bottom as blockers.)

Current path (what it actually takes today)

Delegating claude-release under an SE root and getting it to sign a verifiable commit required:

  1. auths id agent add --label claude-release --key main --curve ed25519 --scope sign_commit --scope sign_release --expires-in 2592000 — fingerprint the delegation. (Must set AUTHS_PASSPHRASE to a 12+ char value first, or the grandfathered cached passphrase is silently reused and rejected by the length policy.)
  2. auths key copy-backend --alias claude-release --dst-backend file --dst-file <keys.enc> — move the agent key into a file keychain.
  3. auths key copy-backend --alias claude-release--next-0 ... — again for the pre-committed next key (easy to forget; signing half-works without it).
  4. Hand the passphrase to the headless environment (write it to a file the agent reads).
  5. Materialize an agent delegate-machine registry — cp -R ~/.auths <agent-registry>, then rewrite refs/auths/registry via git read-tree / rm --cached / write-tree / commit-tree / update-ref to drop every identity subtree except the agent's dip. This is raw git plumbing no user should ever run, and it's undocumented.
  6. Stamp <agent-registry>/commit-trailers with the agent's Auths-Id / Auths-Device / Auths-Anchor-Seq (the seq read out of the root's nested state.json).
  7. Regenerate .auths/ci-bundle.json so CI's bundle evidence carries the new delegation anchors.
  8. Ensure the repo's traveling refs/auths/registry is pushed so verifiers can resolve the delegation.
  9. Sign — with a pile of env + config overrides that are non-obvious:
    export AUTHS_KEYCHAIN_BACKEND=file AUTHS_KEYCHAIN_FILE=<keys.enc> AUTHS_PASSPHRASE=<pass>
    export AUTHS_HOME=<agent-registry> AUTHS_REPO=<agent-registry>
    GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.signingkey GIT_CONFIG_VALUE_0=auths:claude-release \
      auths sign HEAD --scope sign_commit
    env -u AUTHS_HOME -u AUTHS_REPO auths verify HEAD
    (The GIT_CONFIG trick is required because auths sign's amend reads the repo's user.signingkey, not the flag/env, so it fails "Key not found" without it.)

Nine steps, two of which (5, 6) are git plumbing and manual JSON spelunking. Nobody will do this correctly unaided.

What "done" looks like — one command

auths agent provision --label claude-release \
  --scope sign_commit,sign_release --expires-in 30d \
  --backend file --out ~/.auths-claude

...prints (and writes to ~/.auths-claude/env.sh):

✓ Delegated claude-release (did:keri:EO1cBsYo…) under did:keri:EB5cPHY0… — scopes: sign_commit, sign_release; expires 2026-08-17
✓ Agent keychain: ~/.auths-claude/keys.enc (file backend)
✓ Agent registry:  ~/.auths-claude/registry (delegate machine)
✓ CI bundle refreshed, traveling ref synced

To sign as this agent:
  source ~/.auths-claude/env.sh
  auths sign <ref>            # scope inferred; --scope to override
Revoke anytime:
  auths id agent revoke did:keri:EO1cBsYo… --key main

And signing itself becomes just:

source ~/.auths-claude/env.sh
auths sign HEAD

Plan

A. New auths agent provision command that internally runs steps 1–8 as one atomic operation:

  • Delegate (step 1), auto-copying both current + next keys into the chosen backend (steps 2–3) — no separate copy-backend calls, no "forgot the next key".
  • Own the delegate-machine materialization (step 5) in SDK code, not user git plumbing. Chain-style machine construction already exists in auths-mcp-gateway/src/chain.rs (materialize_agent_machine) — promote that into auths-sdk and reuse it here. Users should never touch refs/auths/registry by hand.
  • Write the commit-trailers file (step 6) from resolved KEL state, not hand-copied JSON.
  • Regenerate the CI bundle + confirm the traveling ref (steps 7–8) as part of provisioning, with a clear note when a push is needed.
  • Emit a single env.sh capturing every var, and (optionally) the passphrase handling (--passphrase-file, or generate + store one).

B. Make auths sign respect its own signer selection so step 9's GIT_CONFIG_COUNT hack dies: auths sign --signer auths:<alias> (or the AUTHS_SIGNING_KEY env) should drive the amend directly instead of falling back to repo user.signingkey.

C. Provisioning presets for the two dominant shapes:

  • --profile ci → file backend, env.sh, bundle refresh, print the exact CI secret/env block.
  • --profile assistant → same but oriented at a long-lived local headless agent (what this session set up).

D. Docs: one page, "Delegate a headless signing agent," that is literally the single command + source env.sh. The current 9-step reality should never appear in docs — it should stop existing.

Acceptance

  • A fresh delegated agent that can sign a verifiable commit (accepted by auths verify through the pinned root) is created by one command + source env.sh, on both a passphrase-file root and a Secure Enclave root.
  • No step requires git read-tree/write-tree/commit-tree/update-ref, hand-editing commit-trailers, or GIT_CONFIG_COUNT overrides.

Blockers / related

  • keychain: Secure Enclave custody fixes — delegation works on SE roots, software keys are never silently discarded #381 fixes the SE custody bugs that make delegation possible on an SE root at all (KEL anchor raw-decrypt; store_key discarding provided material). Provisioning on SE roots depends on it.
  • Follow-ups this ergonomics work should also absorb: key export refuses per-backend instead of per-key (software keys in an SE backend can't export — worked around via copy-backend); a mid-ceremony SE auth failure leaves a partially-anchored delegation (keys stored, later seals missing) — provisioning must be atomic or resumable; the passphrase min-length policy silently rejects the cached dev.auths.passphrase keychain item.

🤖 Generated with Claude Code

Activity

  1. bordumb commented on Jul 18, 2026

    @bordumb
    ContributorAuthor

    Field report: drove two full releases headlessly with a provisioned agent

    Since filing this, I actually provisioned the claude-release agent and used it to run two complete auths releases (v0.1.7, v0.1.8) with zero fingerprints — bump commit + release tag signed by the agent (sign_release scope), and CI's verify-commits accepted the agent-signed commits against the pinned root. So the north star ("one command → releases with no fingerprint") is real and proven, not aspirational. That said, daily use surfaced friction beyond the provisioning ceremony that auths agent provision (or adjacent fixes) should absorb:

    New pain points, from actually signing with the agent

    1. auths sign ignores its own signer selection — every sign needs a GIT_CONFIG hack. auths sign <ref> --scope … amends the commit, and the amend reads the repo's user.signingkey, not a flag/env. Without an override it fails [AUTHS-E5909] keychain unavailable: Key not found. Every single sign in the release flow needed:

      GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.signingkey GIT_CONFIG_VALUE_0=auths:claude-release auths sign HEAD --scope sign_release

      This is item B above, now with a concrete, repeated failure mode. auths sign --signer auths:<alias> (or AUTHS_SIGNING_KEY) must drive the amend directly.

    2. The commit-trailers file is a static cache, not live resolution. Signing stamps Auths-Id/Auths-Device from the literal $AUTHS_HOME/commit-trailers file. Switching between root-signing and agent-signing (or a stale agent registry) silently stamps the wrong device, and the commit then fails verify with SignerKeyMismatch. Provisioning writes this once; nothing keeps it correct as you switch identities. (This also caused a separate human-facing bug — see identity: signer resolution must not stamp a delegated device it can't sign as #383.)

    3. Dual-registry AUTHS_HOME juggling. To sign as the agent you must point AUTHS_HOME/AUTHS_REPO at the agent's delegate-machine registry (so the resolver finds the agent as signer), but to verify you must env -u AUTHS_HOME -u AUTHS_REPO back to the default. Getting this wrong yields confusing verdicts. provision's env.sh should encapsulate it, and auths verify shouldn't need the unset dance.

    4. .auths/ci-bundle.json must be regenerated after every new delegation — and committed. CI verifies agent-signed commits against the committed bundle; a bundle predating the delegation fails Root never anchored this device's delegated inception. For a release repo this is a required, easy-to-forget step provision should perform (and re-run on re-delegation).

    Blocker status update

    One addition to the plan

    auths agent provision should mark the delegation agent-role and rely on #383's rule, so the human's identity is never affected — and it should own the ci-bundle.json refresh (point 4) and emit the env.sh that hides the dual-registry env (point 3). Combined with fix B (point 1), signing collapses to source env.sh && auths sign <ref>.

    🤖 Generated with Claude Code

  2. bordumb commented on Jul 20, 2026

    @bordumb
    ContributorAuthor

    Field report #2: v0.1.13 release — the signing gap is now tags, not just commits

    Drove another full headless release (v0.1.13) with the provisioned claude-release agent. The commit half is solid — but the release surfaced a gap this issue doesn't cover yet, plus a concrete re-confirmation of item B.

    New: agent-signed tags have no path — and "signed releases" needs them

    The release commit signs fine via the documented dance. The tag does not. git tag -a (what 1_github.py --push and any annotated tag runs) routes through tag.forceSignAnnotated=true → gpg.ssh.program=…/auths-sign (the SE wrapper) → dies headless:

    error: [ERROR] [AUTHS-E5910] key decryption failed: Signing failed: Secure Enclave key operation failed
    error: unable to sign the tag
    ERROR: git tag failed (exit 128)
    

    There is no auths-native way to agent-sign a tag: auths sign --scope is "Commit-only" (per --help), and artifact signing (--key/--device-key → .auths.json) has no scope and isn't a git object. So the only headless path is to create an unsigned tag and lean on the signed commit for provenance:

    git -c tag.forceSignAnnotated=false tag -a vX.Y.Z -m "release: release for X.Y.Z"

    If the product promise is signed releases, item B (signer selection) should extend to tags — e.g. auths sign refs/tags/vX.Y.Z --scope sign_release, or auths agent provision should emit an env that makes git tag -s resolve to the agent key headlessly. Right now a released tag carries no agent signature.

    The release scripts aren't agent-aware either

    A provisioned --profile ci/assistant agent still can't use the repo's own release tooling: just release runs cargo + assumes the SE fingerprint prompt, and 1_github.py --push hits the tag-signing trap above and only pushes the tag (never main). I had to bypass both and hand-roll the tag + main push. Whatever provision ships, --profile ci should also make scripts/releases/* work under the agent — otherwise the "one command → headless releases" story stops one step short of the actual release command. (I rewrote scripts/releases/README.md into a working headless runbook in the meantime, but that's a patch over the tooling gap.)

    Item B, re-confirmed live in v0.1.13

    Every auths sign in the release still needed the GIT_CONFIG hack; without it, [AUTHS-E5909] keychain unavailable: Key not found. auths sign has no --signer flag yet (--help shows only --key/--device-key/--scope/--note), so the amend still falls back to the repo's user.signingkey:

    GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.signingkey GIT_CONFIG_VALUE_0=auths:claude-release \
      auths sign HEAD --scope sign_release

    Net: the provisioning ceremony is the headline, but releasing with the provisioned agent still needs (1) a tag-signing story, (2) headless-aware release scripts, and (3) B's --signer. Suggest folding (1) and (2) into this issue's scope, since a "headless signing agent" that can't sign the release tag or run the release script isn't done.

    🤖 Field report from a Claude Code session that cut v0.1.13.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions