/human-confirm-gate
Mechanical confirmation for destructive Git and repository administration commands.
$ golems-cli skills install human-confirm-gateUpdated today
Status: source implementation; install only from hooks-live after lead review/merge.
A chat turn, send_to, sender stamp, or model assertion is never approval.
The Claude PreToolUse hook classifies shell commands with _shared/shell_parse.
Simple literal assignments resolve command/argument words (including #500);
unresolved protected executable scope and xargs Git/GH/shell executors deny.
It blocks force/lease pushes, positive-force refspecs, remote deletes, mirror/prune,
filter-repo/filter-branch/replace (conservatively even local rewrites), replacement
ref pushes, repo visibility/delete/archive/rename/default-branch/forced-sync,
and GitHub settings/ruleset/protection/delete/transfer API mutations.
Ordinary pushes with redirects/pipes, PR comments/reviews/labels, read-only
Git global options and config overrides pass. Unknown cwd/executable/config
scope denies when it could affect a protected operation. The gate inspects
known wrappers, shell -c/trap and literal stdin payloads, Git executors and
same-call config mutations; opaque shell stdin sources deny. Unclosed input
and policy/import/runtime errors deliberately deny (exit 2).
Fleet false positives. tests/fleet_commands.json holds about 5,000
values-stripped shapes of real Claude and Codex Bash commands. The gate must
deny at most 0.5% of them, and every deny carries a reason category. What
the gate treats as data:
- Quoted heredoc bodies (
<<'EOF'): prose such as backticks or apostrophes is not a substitution. This applies only when the shared parser's line view and a bash-faithful scan agree on every heredoc; otherwise the whole command is scanned. Unquoted bodies are never masked. - Prose arguments with an apostrophe, unless they carry a Git/GH payload.
- Script operands:
bash filereads its script from the file, andbash -nruns nothing. A shell with no operand,-s,-, a stdin path or process substitution reads stdin, so it is opaque and denies. A bare interactive shell denies too. Options are read per shell (bash/sh, dash, ksh, zsh, fish), including which ones take a value. An unknown long option denies. fish's-C/--init-command/--commandare code, like-c. - Prefix assignments (
X="$HOME/y" cmd) are not the executable. command -v/-Vonly looks a name up.- Local-only git commands (merge-base, check-ignore, …): no alias lookup.
gh apiwith a shell id: the endpoint may use a shell value only as a whole id segment under pulls/issues/comments/reviews/commits/runs/jobs/ check-runs of a literalrepos/<owner>/<repo>. A dynamic GET route is a read only if it starts with a literal, is the only endpoint word, and no expansion in the call can word-split. An expansion can split if it is unquoted, or if its quoting was lost through a wrapper or an alias. A word led by an expansion makes the method unknown: the shell value may be a gh flag.- GraphQL: a
$namethe query declares is GraphQL syntax only if every$namein the command is single-quoted, so the shell never expands it. Values of fields other thanquery/operationNameare variables and cannot change the operation.
Git routes:
- Executable names are case-folded (APFS is case-insensitive), so
GIT,Git,GHand absolute paths in any case are the same commands. - git's per-subcommand executables are treated as the matching git subcommand.
- At push time the gate evaluates the effective route: the remote picked by
--repo,branch.<b>.pushRemote,remote.pushDefaultorbranch.<b>.remote, plus that remote's configuredpushrefspecs andmirror, when the command names none. So a stored force/delete refspec or mirror needs a token on the plain push that uses it. (:alone is git's "matching" refspec, not a delete.) - Tokens bind a digest of that route's config (URLs included, values never copied), so changing the remote after issuance invalidates the token.
- Storing a destructive route needs a token too:
git configsetters of a force/deleteremote.<r>.pushor a truemirror;- Write/Edit/MultiEdit whose resulting git config (
.git/config,config.worktree, or the repo's common-dir config) would hold one. The edit is simulated on the current file.
- Other writers to git config (shell redirects, includes) are judged at push
time instead.
Like the other policy gates, this gate runs through the launcher's
--fail-closedmode (#656): a missing or unparseable hook, a launcher crash, a missing pinned interpreter (the installer's/bin/shguard) and a hang past the watchdog budget (manifest timeout 10 s, budget 9 s) all deny with the static reinstall hint. This hook also catches evaluator/import errors itself. Installer status must still be checked before calling this an installed mechanical gate. This only covers Claude tool calls; Codex exec_command is not wired to it.
Trust and issuance
Unsigned file metadata cannot identify a human under the shared macOS UID.
Tokens are SSH-signed JSON, mode 0600, in
~/.config/golems/human-confirm/<32-lowercase-hex-nonce>.json, with an adjacent
.json.sig. The owner provisions mode-0600
~/.config/golems/human-confirm-anchor/allowed_signers out of band, with SSH allowed
signers principals human and lead (separate keys). The human signing key
must be unavailable to agents: a separate owner-controlled signer or hardware
key requiring owner presence. Only the owner creates/signs human tokens;
workers never mint them. A software private key in the shared workspace/HOME
is insufficient. The lead signer must be unavailable to workers; under one UID
this is an operational trust boundary, not enforced key isolation.
Detectable Bash writes/deletes and Write/Edit/MultiEdit/NotebookEdit attempts
to the policy directory deny, including case variants. Monitor commands use
the same evaluator as Bash.
A file copied by an agent cannot gain approval without the issuer's signature.
The same-UID threat frontier remains: this hook is not an OS sandbox; a malicious
process can replace the hook, configuration, or tombstones. Arbitrary interpreter
code, sourced files and external shell startup aliases remain outside static
inspection. Unknown wrappers carrying recognizable protected Git/GH argv deny.
Unregistered tool surfaces remain outside this hook. No owner-origin claim
is made from PID, timestamps, mode, or chat provenance.
Use scripts/golems-confirm <repo> <ref> <action> --session <id> from a
separate owner terminal; lease also needs --sha <full-sha>. See README for
1Password SSH-agent public-key setup and required per-request authorization.
Cached application/all-process authorization is insufficient human proof.
The helper uses ssh-keygen -U to require the agent; ancestry checks are only
advisory detection. The per-request signer prompt is the owner-presence control.
Trust anchor integrity
The anchor lives in its own directory, ~/.config/golems/human-confirm-anchor/,
so locking it never touches the other files under ~/.config/golems.
It holds exactly one plain key for human and at most one for lead.
Options are not allowed, so no cert-authority and no shared principals.
Provisioning (owner terminal):
- Create the directory mode 0700 and place the allowed signers file there.
- Run
scripts/golems-confirm-pinfrom the pinned tree (the golems repo's.worktrees/hooks-live). Do not run it from a dev checkout an agent can edit. - It prints each principal with its key's
SHA256:fingerprint. Compare them with your signer, then typePIN. Only then does it set macOSuchgon the file and the directory and print one pin line. - That line lands in this skill's
anchor.pinsthrough a reviewed PR.
The pin lives in the pinned hooks-live tree, not the policy directory, so clearing flags and rewriting the anchor cannot also re-pin it.
anchor.pins grammar is shared with install-hooks (tests/pin-vectors.json):
ASCII, LF lines; each line is empty, a printable # comment, or 64 lowercase
hex plus an optional label ([A-Za-z0-9._-]+). Anything else voids the pin.
Every token check denies every human and lead token, with the hook's static message, unless all of these hold:
anchor.pinsin the hook tree is byte-identical to its blob at that tree's git HEAD, so uncommitted edits in hooks-live never re-pin. Git is pointed at the tree root explicitly, with object replacement off and discovery stopped at the root. Any repository marker between the pin and the root denies;- the anchor, opened fd-relative with no symlinks, has
uchgon the file and the directory, mode 0700/0600, owner UID and a single link; - its SHA-256 is pinned, and its principals pass the grammar above.
SSH verifies the checked bytes through a pipe and never reopens the path.
Hook imports. The installer runs this gate as <pinned python3> -I -B through the
shared launcher, like every golems Python hook. The launcher loads what
runpy needs first, and adds every hook directory LAST on sys.path, never
first. The gate then:
- takes its own directory and
_sharedoffsys.pathwhile the stdlib loads, and adds them back last; - compiles its modules from source, never reading cached bytecode;
- denies every call while compiled modules, symlinks or shadow packages sit beside its sources.
Installer. install-hooks never activates this gate while anchor.pins
holds no fingerprint. An --update to an unpinned commit unlinks and
deregisters it, and --status reports refused(unpinned). Order: merge, then
the owner pins, then the fingerprint PR, then hooks-live install. Key rotation
means an owner-terminal unlock, review and repin, plus a new pin PR. Never
repin automatically on a mismatch.
What install-hooks --status detects (exit 1). Git runs with
fsmonitor and the untracked cache off, and object replacement disabled:
- tracked edits, and untracked files that are not gitignored, in hooks-live;
- index flags (skip-worktree, assume-unchanged) on any path, and any replace refs;
- for each
requiresPingate, its source dir and_sharedcompared byte-for-byte with HEAD. That reports changed, missing and extra files, ignored ones included.__pycache__is skipped because the gate never reads it; - a hooks-live HEAD that is not on origin/master;
- a HEAD that differs from the SHA recorded by the last
--apply, or no recorded SHA.
It does not cover ignored files outside the gate's import dirs. It cannot see
a same-UID edit that is reverted before it runs, or tampering with git's own
refs and recorded SHA together. The hook does not inspect its
own source at runtime: a same-UID process that rewrites hooks-live's hook code
or commits there defeats the gate until --status flags it.
Early fail-closed argv checks are defence in depth, not the boundary. They deny:
chflags/SetFilewhose targets are unresolved (glob, brace, variable, substitution,~user), relative under an unknown or wrapper-changed cwd, or able to reach the anchor or pinned tree, including-Ron an anchor ancestor;- flag executors fed targets by
xargs,parallelorfind; - non-reader commands whose literal operands name the anchor directory or
pinned tree (
lnalso of an anchor ancestor), and Write/Edit to either.
They do NOT catch:
- writes through clustered short options, awk/sed write commands, or relative
operands after an unresolvable
cd; gitinside hooks-live;- removing an ancestor of hooks-live (the fail-closed launcher then denies every matching call until reinstall);
- interpreter code (
python -c,osascript).
The anchor's uchg/hash and the committed pin are what catch these outcomes.
By-design over-denies:
chflagswith any glob, or with a relative target under an unknown cwd or a wrapper (e.g.timeout 5 chflags …);--opt=<anchor path>and copying the anchor out with a non-reader.
cd -, pushd/popd stacks and CDPATH searches make cwd unknown.
This is tamper evidence within the tool boundary, not an OS sandbox.
Anchor provisioning/runtime are macOS-only. Unsupported hosts refuse tokens;
CI skips those runtime fixtures explicitly, while structural tests still run.
Prepare a JSON draft in the repo's docs.local/ with these fields:
version: 1;kind:humanorlead;nonce: random 32 lowercase hex.issued_at,expires_at: Unix seconds, maximum TTL 300 seconds, no future issue.command_sha256: SHA256 of exact hookcwd+ NUL + exact Bash command.operations: output ofhooks/commands.py:operations(command, cwd).session_id: the Claude worker session ID from the hook input.collab: collab path, for lead tokens only. Review the exact command, repo, remote, refs and operation classes, then the issuer copies the draft to the token path, chmods 0600, and signs it:
ssh-keygen -Y sign -U -f <issuer-controlled-public-key> -n golems-confirm <nonce>.jsonThe hook verifies raw JSON bytes using ssh-keygen -Y verify; there is no model
judgement. The digest scopes the complete command (including wrappers) and cwd;
operations scope repo/ref/class/lease SHA; session binding prevents cross-worker
reuse. A valid human token permits its entire scoped command. A .spent nonce
is atomically created before capability deletion; keep tombstones permanently.
Even a restored token is denied. Failed execution still consumes approval: get
a fresh token for retries. Concurrent callers cannot consume the same nonce.
Lead tokens
scripts/golems-lead-confirm <repo> --ref refs/heads/<branch> --sha <full-sha> --session <id> [--remote origin]
issues exactly one lead token. The token covers a --force-with-lease push of the CURRENT
branch to its own open, unmerged, same-repository PR, at that PR's exact remote head SHA.
- It refuses main/master and the default branch (in any letter case, even when the default branch is something else), a checkout with more than one GitHub remote, other refs, short SHAs, a remote that is not one GitHub URL, and any push that config turns into something other than a plain lease.
- One rule for both sides. The issuer and the gate share
tokens.lead_scope. - It signs with
~/.config/golems/lead-signer/lead_ed25519(0600 file, 0700 directory;scripts/golems-lead-keygencreates it once). It uses no agent, no 1Password and no owner terminal: leads are agents, and the scope limits are the control. - Logging. Each issuance appends one JSON line to
~/.config/golems/human-confirm/lead-issued.log. It also appends the token'sGOLEMS_CONFIRMline plus a readable- lead-token issued …line (with the GitHubowner/repo) to the OSS collab, always. If either write fails, nothing is issued. - Not isolation. Under one macOS UID the lead key is an operational boundary, not isolation: any process of this user can read the key file.
Lead scope
The signed lead token additionally needs an exact line in its signed collab.
The resolved path must be inside the coordinator's collab directory (see the
private installation handoff for the machine layout). The line contains
GOLEMS_CONFIRM followed by the token JSON sorted by key, compact separators
, and :. Ordinary prose or an unlogged token denies. Signing proves lead
issuance; the collab line logs the exact session/ref/SHA/command authorization.
Only a single literal git [-C <repo>] push --force-with-lease=refs/heads/<branch>:<full-sha> <remote> <source>:refs/heads/<branch> qualifies. Bare force, bare lease, multiple refs,
wrapped/compound commands and other classes require human tokens.
The current local branch must match, source must be HEAD/current branch,
remote must resolve to one GitHub URL, and live read-only GH metadata must show
that exact remote repo's open, unmerged, same-repository PR at the exact lease
SHA, on a non-default branch. The signed session/ref grant asserts worker
ownership; branch names and GitHub author alone do not establish worker identity.
Metadata failures or timeouts deny. Binaries are fixed system Git/SSH, and
gh from fixed, ownership-checked Homebrew locations, avoiding caller PATH substitution. No real destructive operation is used in
verification; integration tests invoke the hook subprocess with signed synthetic
capabilities and disposable repos under docs.local.
Full SKILL.md source — includes LLM directives, anti-patterns, and technical instructions stripped from the Overview tab.
Status: source implementation; install only from hooks-live after lead review/merge.
A chat turn, send_to, sender stamp, or model assertion is never approval.
The Claude PreToolUse hook classifies shell commands with _shared/shell_parse.
Simple literal assignments resolve command/argument words (including #500);
unresolved protected executable scope and xargs Git/GH/shell executors deny.
It blocks force/lease pushes, positive-force refspecs, remote deletes, mirror/prune,
filter-repo/filter-branch/replace (conservatively even local rewrites), replacement
ref pushes, repo visibility/delete/archive/rename/default-branch/forced-sync,
and GitHub settings/ruleset/protection/delete/transfer API mutations.
Ordinary pushes with redirects/pipes, PR comments/reviews/labels, read-only
Git global options and config overrides pass. Unknown cwd/executable/config
scope denies when it could affect a protected operation. The gate inspects
known wrappers, shell -c/trap and literal stdin payloads, Git executors and
same-call config mutations; opaque shell stdin sources deny. Unclosed input
and policy/import/runtime errors deliberately deny (exit 2).
Fleet false positives. tests/fleet_commands.json holds about 5,000
values-stripped shapes of real Claude and Codex Bash commands. The gate must
deny at most 0.5% of them, and every deny carries a reason category. What
the gate treats as data:
- Quoted heredoc bodies (
<<'EOF'): prose such as backticks or apostrophes is not a substitution. This applies only when the shared parser's line view and a bash-faithful scan agree on every heredoc; otherwise the whole command is scanned. Unquoted bodies are never masked. - Prose arguments with an apostrophe, unless they carry a Git/GH payload.
- Script operands:
bash filereads its script from the file, andbash -nruns nothing. A shell with no operand,-s,-, a stdin path or process substitution reads stdin, so it is opaque and denies. A bare interactive shell denies too. Options are read per shell (bash/sh, dash, ksh, zsh, fish), including which ones take a value. An unknown long option denies. fish's-C/--init-command/--commandare code, like-c. - Prefix assignments (
X="$HOME/y" cmd) are not the executable. command -v/-Vonly looks a name up.- Local-only git commands (merge-base, check-ignore, …): no alias lookup.
gh apiwith a shell id: the endpoint may use a shell value only as a whole id segment under pulls/issues/comments/reviews/commits/runs/jobs/ check-runs of a literalrepos/<owner>/<repo>. A dynamic GET route is a read only if it starts with a literal, is the only endpoint word, and no expansion in the call can word-split. An expansion can split if it is unquoted, or if its quoting was lost through a wrapper or an alias. A word led by an expansion makes the method unknown: the shell value may be a gh flag.- GraphQL: a
$namethe query declares is GraphQL syntax only if every$namein the command is single-quoted, so the shell never expands it. Values of fields other thanquery/operationNameare variables and cannot change the operation.
Git routes:
- Executable names are case-folded (APFS is case-insensitive), so
GIT,Git,GHand absolute paths in any case are the same commands. - git's per-subcommand executables are treated as the matching git subcommand.
- At push time the gate evaluates the effective route: the remote picked by
--repo,branch.<b>.pushRemote,remote.pushDefaultorbranch.<b>.remote, plus that remote's configuredpushrefspecs andmirror, when the command names none. So a stored force/delete refspec or mirror needs a token on the plain push that uses it. (:alone is git's "matching" refspec, not a delete.) - Tokens bind a digest of that route's config (URLs included, values never copied), so changing the remote after issuance invalidates the token.
- Storing a destructive route needs a token too:
git configsetters of a force/deleteremote.<r>.pushor a truemirror;- Write/Edit/MultiEdit whose resulting git config (
.git/config,config.worktree, or the repo's common-dir config) would hold one. The edit is simulated on the current file.
- Other writers to git config (shell redirects, includes) are judged at push
time instead.
Like the other policy gates, this gate runs through the launcher's
--fail-closedmode (#656): a missing or unparseable hook, a launcher crash, a missing pinned interpreter (the installer's/bin/shguard) and a hang past the watchdog budget (manifest timeout 10 s, budget 9 s) all deny with the static reinstall hint. This hook also catches evaluator/import errors itself. Installer status must still be checked before calling this an installed mechanical gate. This only covers Claude tool calls; Codex exec_command is not wired to it.
Trust and issuance
Unsigned file metadata cannot identify a human under the shared macOS UID.
Tokens are SSH-signed JSON, mode 0600, in
~/.config/golems/human-confirm/<32-lowercase-hex-nonce>.json, with an adjacent
.json.sig. The owner provisions mode-0600
~/.config/golems/human-confirm-anchor/allowed_signers out of band, with SSH allowed
signers principals human and lead (separate keys). The human signing key
must be unavailable to agents: a separate owner-controlled signer or hardware
key requiring owner presence. Only the owner creates/signs human tokens;
workers never mint them. A software private key in the shared workspace/HOME
is insufficient. The lead signer must be unavailable to workers; under one UID
this is an operational trust boundary, not enforced key isolation.
Detectable Bash writes/deletes and Write/Edit/MultiEdit/NotebookEdit attempts
to the policy directory deny, including case variants. Monitor commands use
the same evaluator as Bash.
A file copied by an agent cannot gain approval without the issuer's signature.
The same-UID threat frontier remains: this hook is not an OS sandbox; a malicious
process can replace the hook, configuration, or tombstones. Arbitrary interpreter
code, sourced files and external shell startup aliases remain outside static
inspection. Unknown wrappers carrying recognizable protected Git/GH argv deny.
Unregistered tool surfaces remain outside this hook. No owner-origin claim
is made from PID, timestamps, mode, or chat provenance.
Use scripts/golems-confirm <repo> <ref> <action> --session <id> from a
separate owner terminal; lease also needs --sha <full-sha>. See README for
1Password SSH-agent public-key setup and required per-request authorization.
Cached application/all-process authorization is insufficient human proof.
The helper uses ssh-keygen -U to require the agent; ancestry checks are only
advisory detection. The per-request signer prompt is the owner-presence control.
Trust anchor integrity
The anchor lives in its own directory, ~/.config/golems/human-confirm-anchor/,
so locking it never touches the other files under ~/.config/golems.
It holds exactly one plain key for human and at most one for lead.
Options are not allowed, so no cert-authority and no shared principals.
Provisioning (owner terminal):
- Create the directory mode 0700 and place the allowed signers file there.
- Run
scripts/golems-confirm-pinfrom the pinned tree (the golems repo's.worktrees/hooks-live). Do not run it from a dev checkout an agent can edit. - It prints each principal with its key's
SHA256:fingerprint. Compare them with your signer, then typePIN. Only then does it set macOSuchgon the file and the directory and print one pin line. - That line lands in this skill's
anchor.pinsthrough a reviewed PR.
The pin lives in the pinned hooks-live tree, not the policy directory, so clearing flags and rewriting the anchor cannot also re-pin it.
anchor.pins grammar is shared with install-hooks (tests/pin-vectors.json):
ASCII, LF lines; each line is empty, a printable # comment, or 64 lowercase
hex plus an optional label ([A-Za-z0-9._-]+). Anything else voids the pin.
Every token check denies every human and lead token, with the hook's static message, unless all of these hold:
anchor.pinsin the hook tree is byte-identical to its blob at that tree's git HEAD, so uncommitted edits in hooks-live never re-pin. Git is pointed at the tree root explicitly, with object replacement off and discovery stopped at the root. Any repository marker between the pin and the root denies;- the anchor, opened fd-relative with no symlinks, has
uchgon the file and the directory, mode 0700/0600, owner UID and a single link; - its SHA-256 is pinned, and its principals pass the grammar above.
SSH verifies the checked bytes through a pipe and never reopens the path.
Hook imports. The installer runs this gate as <pinned python3> -I -B through the
shared launcher, like every golems Python hook. The launcher loads what
runpy needs first, and adds every hook directory LAST on sys.path, never
first. The gate then:
- takes its own directory and
_sharedoffsys.pathwhile the stdlib loads, and adds them back last; - compiles its modules from source, never reading cached bytecode;
- denies every call while compiled modules, symlinks or shadow packages sit beside its sources.
Installer. install-hooks never activates this gate while anchor.pins
holds no fingerprint. An --update to an unpinned commit unlinks and
deregisters it, and --status reports refused(unpinned). Order: merge, then
the owner pins, then the fingerprint PR, then hooks-live install. Key rotation
means an owner-terminal unlock, review and repin, plus a new pin PR. Never
repin automatically on a mismatch.
What install-hooks --status detects (exit 1). Git runs with
fsmonitor and the untracked cache off, and object replacement disabled:
- tracked edits, and untracked files that are not gitignored, in hooks-live;
- index flags (skip-worktree, assume-unchanged) on any path, and any replace refs;
- for each
requiresPingate, its source dir and_sharedcompared byte-for-byte with HEAD. That reports changed, missing and extra files, ignored ones included.__pycache__is skipped because the gate never reads it; - a hooks-live HEAD that is not on origin/master;
- a HEAD that differs from the SHA recorded by the last
--apply, or no recorded SHA.
It does not cover ignored files outside the gate's import dirs. It cannot see
a same-UID edit that is reverted before it runs, or tampering with git's own
refs and recorded SHA together. The hook does not inspect its
own source at runtime: a same-UID process that rewrites hooks-live's hook code
or commits there defeats the gate until --status flags it.
Early fail-closed argv checks are defence in depth, not the boundary. They deny:
chflags/SetFilewhose targets are unresolved (glob, brace, variable, substitution,~user), relative under an unknown or wrapper-changed cwd, or able to reach the anchor or pinned tree, including-Ron an anchor ancestor;- flag executors fed targets by
xargs,parallelorfind; - non-reader commands whose literal operands name the anchor directory or
pinned tree (
lnalso of an anchor ancestor), and Write/Edit to either.
They do NOT catch:
- writes through clustered short options, awk/sed write commands, or relative
operands after an unresolvable
cd; gitinside hooks-live;- removing an ancestor of hooks-live (the fail-closed launcher then denies every matching call until reinstall);
- interpreter code (
python -c,osascript).
The anchor's uchg/hash and the committed pin are what catch these outcomes.
By-design over-denies:
chflagswith any glob, or with a relative target under an unknown cwd or a wrapper (e.g.timeout 5 chflags …);--opt=<anchor path>and copying the anchor out with a non-reader.
cd -, pushd/popd stacks and CDPATH searches make cwd unknown.
This is tamper evidence within the tool boundary, not an OS sandbox.
Anchor provisioning/runtime are macOS-only. Unsupported hosts refuse tokens;
CI skips those runtime fixtures explicitly, while structural tests still run.
Prepare a JSON draft in the repo's docs.local/ with these fields:
version: 1;kind:humanorlead;nonce: random 32 lowercase hex.issued_at,expires_at: Unix seconds, maximum TTL 300 seconds, no future issue.command_sha256: SHA256 of exact hookcwd+ NUL + exact Bash command.operations: output ofhooks/commands.py:operations(command, cwd).session_id: the Claude worker session ID from the hook input.collab: collab path, for lead tokens only. Review the exact command, repo, remote, refs and operation classes, then the issuer copies the draft to the token path, chmods 0600, and signs it:
ssh-keygen -Y sign -U -f <issuer-controlled-public-key> -n golems-confirm <nonce>.jsonThe hook verifies raw JSON bytes using ssh-keygen -Y verify; there is no model
judgement. The digest scopes the complete command (including wrappers) and cwd;
operations scope repo/ref/class/lease SHA; session binding prevents cross-worker
reuse. A valid human token permits its entire scoped command. A .spent nonce
is atomically created before capability deletion; keep tombstones permanently.
Even a restored token is denied. Failed execution still consumes approval: get
a fresh token for retries. Concurrent callers cannot consume the same nonce.
Lead tokens
scripts/golems-lead-confirm <repo> --ref refs/heads/<branch> --sha <full-sha> --session <id> [--remote origin]
issues exactly one lead token. The token covers a --force-with-lease push of the CURRENT
branch to its own open, unmerged, same-repository PR, at that PR's exact remote head SHA.
- It refuses main/master and the default branch (in any letter case, even when the default branch is something else), a checkout with more than one GitHub remote, other refs, short SHAs, a remote that is not one GitHub URL, and any push that config turns into something other than a plain lease.
- One rule for both sides. The issuer and the gate share
tokens.lead_scope. - It signs with
~/.config/golems/lead-signer/lead_ed25519(0600 file, 0700 directory;scripts/golems-lead-keygencreates it once). It uses no agent, no 1Password and no owner terminal: leads are agents, and the scope limits are the control. - Logging. Each issuance appends one JSON line to
~/.config/golems/human-confirm/lead-issued.log. It also appends the token'sGOLEMS_CONFIRMline plus a readable- lead-token issued …line (with the GitHubowner/repo) to the OSS collab, always. If either write fails, nothing is issued. - Not isolation. Under one macOS UID the lead key is an operational boundary, not isolation: any process of this user can read the key file.
Lead scope
The signed lead token additionally needs an exact line in its signed collab.
The resolved path must be inside the coordinator's collab directory (see the
private installation handoff for the machine layout). The line contains
GOLEMS_CONFIRM followed by the token JSON sorted by key, compact separators
, and :. Ordinary prose or an unlogged token denies. Signing proves lead
issuance; the collab line logs the exact session/ref/SHA/command authorization.
Only a single literal git [-C <repo>] push --force-with-lease=refs/heads/<branch>:<full-sha> <remote> <source>:refs/heads/<branch> qualifies. Bare force, bare lease, multiple refs,
wrapped/compound commands and other classes require human tokens.
The current local branch must match, source must be HEAD/current branch,
remote must resolve to one GitHub URL, and live read-only GH metadata must show
that exact remote repo's open, unmerged, same-repository PR at the exact lease
SHA, on a non-default branch. The signed session/ref grant asserts worker
ownership; branch names and GitHub author alone do not establish worker identity.
Metadata failures or timeouts deny. Binaries are fixed system Git/SSH, and
gh from fixed, ownership-checked Homebrew locations, avoiding caller PATH substitution. No real destructive operation is used in
verification; integration tests invoke the hook subprocess with signed synthetic
capabilities and disposable repos under docs.local.
Not yet evaluated on current models
Results will appear here once runs with a recorded model are published.
Changelog entries are derived from skill version updates. Full cascading changelog (Phase 4D) coming soon.
- +Initial release to Golems skill library