Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .claude/skills/verification/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ For every `FAIL` line in the output, apply the fix described below, then re-run
| `make lint` | Run `make lint` to see current errors. Fix each one - do not add `//nolint:` without a comment explaining why. Re-run until clean. |
| `test-race` | Find the failing test(s) in the output above the gate line. Fix the root cause. Do NOT suppress the race detector with `runtime.GOMAXPROCS(1)` or similar hacks. |
| coverage below 90% | Open `.reports/coverage-<module>.html` for the listed package. Add tests for uncovered lines. Aim for lines that test real behaviour, not just coverage points. |
| GPG signatures | Do NOT re-sign automatically. Report the unsigned commits to the user, show the exact commits, and ask for explicit confirmation before running the re-sign command from CLAUDE.md. |
| GPG signatures | Do NOT re-sign automatically. Report the unsigned commits to the user, show the exact commits, and ask for explicit confirmation before running the re-sign command from CLAUDE.md. GitHub-generated "Merge pull request" commits are ignored by the gate (their `web-flow` key is often expired/untrusted locally and they can't be re-signed without diverging from the remote). |
| CHANGES.md missing `## Development` | Add the section at the top of CHANGES.md with one bullet per user-visible change introduced on this branch. |
| CHANGES.md has no bullet entries | Add bullets under `## Development` - one per logical change: what changed and why, in plain English. |
| docs not updated | Identify which new or changed behaviour is undocumented. Add a section to `README.md` (user-facing changes) or the relevant file under `docs/` (architecture/config changes). |
Expand Down Expand Up @@ -73,3 +73,6 @@ git log --format="%G? %h %s" | head -20
- Every line starts with `G` - report "All commits signed. Verification complete."
- Any line starts with `N` or `B` - list those commits and ask the user for confirmation
before re-signing. Use the re-sign command from CLAUDE.md.
- Lines whose subject begins with `Merge pull request` may carry `E` (expired
GitHub `web-flow` key) - ignore them. Re-signing them would diverge from
the remote and serves no purpose.
5 changes: 4 additions & 1 deletion .scripts/verification.sh
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,10 @@ else
COMMITS=$(git log --format="%G? %h %s" | head -20 || true)
fi

UNSIGNED=$(printf '%s\n' "$COMMITS" | { grep -v "^G " || true; })
# Ignore GitHub-generated "Merge pull request" commits: they are signed by
# GitHub's web-flow key, which is often expired/untrusted locally and shows
# as 'E'. We can't re-sign them anyway (they would diverge from the remote).
UNSIGNED=$(printf '%s\n' "$COMMITS" | { grep -v "^G " || true; } | { grep -v "Merge pull request" || true; })
if [ -z "$UNSIGNED" ]; then
gate_pass "all commits on this branch are GPG-signed"
else
Expand Down
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@
use `export LOCKSMITH_SESSION=$(locksmith session ensure --quiet)`)
and includes usage examples for all three shipped vault types
(`gopass`, `keychain`, `1password`).
- Agent templates now document an MCP-wrapper fallback: when a locksmith-wrapped MCP server fails, the agent parses `--header` and `--env` templates from its own MCP config, resolves them via `locksmith get`, and runs the server directly. Templates also whitelist `session ensure`, `serve`, and `get` as the only runtime locksmith commands.
- The verification GPG gate now ignores GitHub-generated "Merge pull request" commits: their `web-flow` signature is often expired locally and they cannot be re-signed without diverging from the remote.
- Agent templates now document a recovery prompt for external HTTP API auth failures: on the first 401/403 for a hostname, the agent asks the user to (1) Skip, (2) provide a locksmith key or vault+path, or (3) paste the token directly. Resolved tokens are cached per hostname for the rest of the session; Skip suppresses further prompts for that hostname.
- Tightened the runtime-whitelist, MCP-fallback, and external-API auth-failure sections in agent templates (~45% shorter; same semantics). The MCP fallback now opens with an explicit "execute these steps; do not ask the user clarifying questions" directive so smaller models commit to the recovery path instead of pausing for confirmation. Validated across multiple parallel sub-agent runs on Sonnet and Haiku covering both flows.

## Version v0.3.0 - 2026-05-15

Expand Down
119 changes: 119 additions & 0 deletions internal/initflow/agents/claude/templates/claude_md.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,125 @@ Just call `locksmith get` directly.
locksmith get --vault keychain --path "My API Token"
locksmith get --vault op --path "op://Personal/GitHub/token"

## Locksmith Commands (runtime whitelist)

Use ONLY these at runtime:

- `locksmith session ensure --quiet` - obtain/refresh a session ID
- `locksmith serve` - start the daemon (rare; `session ensure` spawns it)
- `locksmith get --key <alias>` or `--vault <name> --path <path>` - fetch a secret

Never call `init`, `vault`, `plugins`, `config`, `reload`, `mcp`, or any other
subcommand - those are for the human operator.

## MCP Servers and Locksmith

MCP config is in your agent's own settings (e.g. `~/.claude.json`), NOT in
locksmith's config. Read it from there.

- `command` != `"locksmith"` -> run as written; act as if locksmith does not
exist for this server.
- `command` == `"locksmith"` with `args` starting `["mcp", "run", ...]` ->
locksmith-wrapped. If it fails or returns an auth error, apply the fallback.

### Fallback for locksmith-wrapped MCP servers

These steps are mandatory whenever a locksmith-wrapped server fails -
startup error, auth error, anything. Execute them; do not ask the user
clarifying questions.

1. Parse args after `mcp run`: `--header Name=<tpl>`, `--env VAR=<tpl>`,
`--url <url>`, tokens after `--`. Templates: `{key:<alias>}` or
`{vault:<name> path:<value>}`.
2. Resolve: `{key:alias}` -> `locksmith get --key alias`;
`{vault:name path:p}` -> `locksmith get --vault name --path p`.
3. Run directly: local = export env, exec post-`--` cmd; proxy = HTTP to
`--url` with resolved headers (no process to spawn).
4. Still denied? Ask user (replace `<MCP_NAME>`; use real header/arg names):

> Enter credentials for MCP `<MCP_NAME>`:
>
> headers: {"Authorization": "Bearer ...", "X-Request-ID": "..."}
> args: --arg1 value1 --flag2

Parse `headers`/`args` and retry.

If `locksmith get` itself fails, surface the error verbatim. Do NOT fall
through to the credential prompt - it is for auth-denial only.

### Example

"github": {
"command": "locksmith",
"args": ["mcp", "run", "--url", "https://api.example.com/mcp",
"--header", "Authorization=Bearer {key:gh-token}"]
}

Fallback:

TOKEN=$(locksmith get --key gh-token)
# then call https://api.example.com/mcp with: Authorization: Bearer $TOKEN

Local-mode entry (env via `{vault:name path:p}` + binary after `--`): resolve
with `locksmith get --vault name --path p`, export, then exec the post-`--` cmd.

## External APIs - try Locksmith on auth failure

On the first auth/access error for an external API's hostname this session,
ask the user once. "Auth/access error" = HTTP `401`/`403`, or body containing
"invalid token"/"unauthorized".

Show (substitute `<HOSTNAME>`):

> A token for `<HOSTNAME>` may be in locksmith. Choose one:
>
> 1. Skip - do not ask again for `<HOSTNAME>` this session.
> 2. Locksmith - paste a key alias OR a `vault+path` pair; I will run
> `locksmith get` and retry.
> 3. Other - paste the token directly, or describe what to do.

Scope = hostname (e.g. `api.github.com`). Paths share the scope.

Do NOT prompt if: the call succeeded; you have a working token for this
hostname (reuse it); the user already chose Skip this session; or this is a
subsequent failure for the same hostname (first failure only).

### Handling each choice

- **Skip**: record hostname as skipped; never re-prompt; continue without it.
- **Locksmith**: parse and run:
- `gh-token` or `key:gh-token` -> `locksmith get --key gh-token`
- `vault:keychain path:gh/tok` or `--vault keychain --path gh/tok` ->
`locksmith get --vault keychain --path gh/tok`

Use the value as the bearer token and retry. Cache $TOKEN for further calls
to the same hostname this session; do not re-run `locksmith get`. If
`locksmith get` itself fails, surface the error verbatim - do NOT silently
fall back.
- **Other**: free-text. If it looks like a token, use it. Otherwise follow
the instructions literally.

Never re-prompt for the same hostname this session, regardless of choice.

### Disambiguation vs MCP fallback

If the failing call originated from an MCP entry whose `command` is
`"locksmith"` with `args` starting `["mcp", "run", ...]`, use the MCP
fallback. Otherwise use this flow. Do not consult locksmith's config file.

### Example

`GET https://api.github.com/user` -> 401. First failure for `api.github.com`:

> A token for `api.github.com` may be in locksmith. Choose one: 1. Skip ...

User: `gh-token` ->

TOKEN=$(locksmith get --key gh-token)
curl -H "Authorization: Bearer $TOKEN" https://api.github.com/user

Cache $TOKEN for further `api.github.com` calls this session.

- Sessions expire automatically by TTL; no manual cleanup needed.
- Pass `LOCKSMITH_SESSION` to sub-agents to share the parent session.
<!-- LOCKSMITH_END -->
119 changes: 119 additions & 0 deletions internal/initflow/agents/claude/templates/claude_skill.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,124 @@ prompt, so no manual session step is needed.
locksmith get --vault keychain --path "My API Token"
locksmith get --vault op --path "op://Personal/GitHub/token"

**Locksmith Commands (runtime whitelist)**

Use ONLY these at runtime:

- `locksmith session ensure --quiet` - obtain/refresh a session ID
- `locksmith serve` - start the daemon (rare; `session ensure` spawns it)
- `locksmith get --key <alias>` or `--vault <name> --path <path>` - fetch a secret

Never call `init`, `vault`, `plugins`, `config`, `reload`, `mcp`, or any other
subcommand - those are for the human operator.

**MCP Servers and Locksmith**

MCP config is in your agent's own settings (e.g. `~/.claude.json`), NOT in
locksmith's config. Read it from there.

- `command` != `"locksmith"` -> run as written; act as if locksmith does not
exist for this server.
- `command` == `"locksmith"` with `args` starting `["mcp", "run", ...]` ->
locksmith-wrapped. If it fails or returns an auth error, apply the fallback.

**Fallback for locksmith-wrapped MCP servers**

These steps are mandatory whenever a locksmith-wrapped server fails -
startup error, auth error, anything. Execute them; do not ask the user
clarifying questions.

1. Parse args after `mcp run`: `--header Name=<tpl>`, `--env VAR=<tpl>`,
`--url <url>`, tokens after `--`. Templates: `{key:<alias>}` or
`{vault:<name> path:<value>}`.
2. Resolve: `{key:alias}` -> `locksmith get --key alias`;
`{vault:name path:p}` -> `locksmith get --vault name --path p`.
3. Run directly: local = export env, exec post-`--` cmd; proxy = HTTP to
`--url` with resolved headers (no process to spawn).
4. Still denied? Ask user (replace `<MCP_NAME>`; use real header/arg names):

> Enter credentials for MCP `<MCP_NAME>`:
>
> headers: {"Authorization": "Bearer ...", "X-Request-ID": "..."}
> args: --arg1 value1 --flag2

Parse `headers`/`args` and retry.

If `locksmith get` itself fails, surface the error verbatim. Do NOT fall
through to the credential prompt - it is for auth-denial only.

**Example**

"github": {
"command": "locksmith",
"args": ["mcp", "run", "--url", "https://api.example.com/mcp",
"--header", "Authorization=Bearer {key:gh-token}"]
}

Fallback:

TOKEN=$(locksmith get --key gh-token)
# then call https://api.example.com/mcp with: Authorization: Bearer $TOKEN

Local-mode entry (env via `{vault:name path:p}` + binary after `--`): resolve
with `locksmith get --vault name --path p`, export, then exec the post-`--` cmd.

**External APIs - try Locksmith on auth failure**

On the first auth/access error for an external API's hostname this session,
ask the user once. "Auth/access error" = HTTP `401`/`403`, or body containing
"invalid token"/"unauthorized".

Show (substitute `<HOSTNAME>`):

> A token for `<HOSTNAME>` may be in locksmith. Choose one:
>
> 1. Skip - do not ask again for `<HOSTNAME>` this session.
> 2. Locksmith - paste a key alias OR a `vault+path` pair; I will run
> `locksmith get` and retry.
> 3. Other - paste the token directly, or describe what to do.

Scope = hostname (e.g. `api.github.com`). Paths share the scope.

Do NOT prompt if: the call succeeded; you have a working token for this
hostname (reuse it); the user already chose Skip this session; or this is a
subsequent failure for the same hostname (first failure only).

**Handling each choice**

- **Skip**: record hostname as skipped; never re-prompt; continue without it.
- **Locksmith**: parse and run:
- `gh-token` or `key:gh-token` -> `locksmith get --key gh-token`
- `vault:keychain path:gh/tok` or `--vault keychain --path gh/tok` ->
`locksmith get --vault keychain --path gh/tok`

Use the value as the bearer token and retry. Cache $TOKEN for further calls
to the same hostname this session; do not re-run `locksmith get`. If
`locksmith get` itself fails, surface the error verbatim - do NOT silently
fall back.
- **Other**: free-text. If it looks like a token, use it. Otherwise follow
the instructions literally.

Never re-prompt for the same hostname this session, regardless of choice.

**Disambiguation vs MCP fallback**

If the failing call originated from an MCP entry whose `command` is
`"locksmith"` with `args` starting `["mcp", "run", ...]`, use the MCP
fallback. Otherwise use this flow. Do not consult locksmith's config file.

**Example**

`GET https://api.github.com/user` -> 401. First failure for `api.github.com`:

> A token for `api.github.com` may be in locksmith. Choose one: 1. Skip ...

User: `gh-token` ->

TOKEN=$(locksmith get --key gh-token)
curl -H "Authorization: Bearer $TOKEN" https://api.github.com/user

Cache $TOKEN for further `api.github.com` calls this session.

Never hardcode secrets. Never cache secrets outside of locksmith.
<!-- LOCKSMITH_END -->
Loading
Loading