Other

/tmp-block

Guard denying durable writes to /tmp, /private/tmp, /var/folders, $TMPDIR; enforces <repo>/.worktrees/<name>. Triggers: tmp guard, TMP-BLOCK deny, WORKTREE-CONVENTION deny, bypass ledger. NOT for storage policy.

$ golems-cli skills install tmp-block

Updated today

Hook-carried skill. The enforcing artifact is hooks/tmp-block-pretooluse.py (PreToolUse on Write|Edit|NotebookEdit|Bash); this page is its contract and audit guide.

Wired location: ~/.claude/hooks/tmp-block, a symlink into the pinned, locked golems/.worktrees/hooks-live tree (GO-5), never into a working tree, which would make the live fleet-wide guard whatever branch happens to be checked out. Install with scripts/hooks/install-hooks.sh --host <mbp|m1> --apply; --status reports drift. A merge does not deploy this hook; --update --apply moves the pin. The legacy copy installer scripts/install.sh is described in hooks/INSTALL.md.

Both host manifests run this policy gate through the installed copy of golems-fail-open.py --fail-closed (#488). A missing, unreadable, syntax-broken or crashed hook produces a static, value-free denial with the reinstall hint ! bash ~/Gits/golems/scripts/hooks/install-hooks.sh --host <host> --update --apply. Flag this to the user: the blocked agent cannot run Bash recovery. A human runs the command from the prompt or an outside terminal (omit ! in a terminal). The installer clears the missing pin's locked registration before recreating it; --update also replaces a broken existing pin. Local damage inside hooks-live refuses until a human inspects it and re-runs with --restore-live. The prompt's ! bypass is the #411 assumption, not live-verified here. Failed-hook output is discarded; legitimate allow/deny results and advisories pass through. This does not change the payload-level advisory rules below. Git-guardian's pre_tool_use.py and human-confirm are the other policy gates; other gates retain the default fail-open mode. A /bin/sh guard around the registered command denies when the pinned interpreter or the launcher cannot start; the registration itself must exist to enforce this boundary; lead installs through hooks-live after review/merge. A hang, import-time included, is cut by the launcher's --budget watchdog (5 s manifest timeout, 4 s budget): the hook's process group is killed and the call denied before the harness timeout, which would allow. Known residuals: registration/launcher edits can disable enforcement, a hook subprocess that leaves the process group outlives the kill, and only Python-level stdout/stderr is captured.

Scope

A PreToolUse guard. The deny covers Write/Edit and Bash writes. The worktree location <repo>/.worktrees/<name> is the ratified convention.

Why it exists (S04, weave 2026-06-07 Phase-2 Fix-3)

A worker ran Write(/tmp/orqi-tts-answer-msg.md); the era's /tmp guard half-fired (validated then allowed) — a hook validation error let the durable write through, and Etan caught it live ("Wait, why are we writing those things in temp?"). Class evidence: ≥24 /tmp anti-pattern findings, 4 proven costs in 24h (reboot-wiped leak worksheet, a harness fix stranded in a prunable /tmp worktree, Etan's own terminal burned by a /tmp-worktree-held branch). Adversary verdict: KEEP, with the deny extended to the canonicalized path-CLASS and to Bash writes, "else it's one directory away from useless."

The contract — two-valued (Etan, by voice, 2026-08-17)

"none of y'all would be able to write to temp, but also not ask me so we don't get agent stuck."

Provably outside the temp class -> ALLOW. Provably inside, or pointing at a temp location -> DENY, with a reason the agent can act on. This hook NEVER emits a PreToolUse prompt.

GO-5 E2 (2026-09-25) softened the unknowns. A target the hook cannot read statically, in a command that shows no temp hint (mktemp, TMPDIR*, /tmp, /var/folders), is ALLOWED with a TMP-BLOCK advisory in hookSpecificOutput.additionalContext (the channel the model reads), and so is a hook error on a payload with no temp hint. Denying those blocked honest work (a conditional cd … && P=<static path> assignment, an unset variable) without evidence of a temp write.

Temp hints (#226 r2): mktemp, mkdtemp, tempfile, gettempdir, .tmpdir( (Node os.tmpdir()), DARWIN_USER_TEMP_DIR, TMPDIR*, $TMP, $TEMP, /tmp, /var/folders.

Residual (documented, not closed): the hint check is lexical. A command that builds a temp path without naming one (P=$(printf '/t''mp'), a base64-decoded path, a value read from a file or from another command's output) gets the advisory, not a refusal. The ledger and the temp-dir scan remain the detectors for that class; closing it statically would mean refusing every unknown value again, which is what E2 removed.

A prompt suspends the pane until a human answers it, and a headless Codex or Cursor worker has no human in its pane at all — 2026-08-14/17 lost hours to ~/Documents probes stranding panes overnight. A deny comes back as a readable error the agent reroutes around by itself, so the residual failure mode is the recoverable one.

SurfaceBehavior
Write/Edit/NotebookEdit into the classDENY + redirect to the repo / docs.local/
Bash output redirect (>, >>, &>), incl. heredoc+redirectDENY
Bash tee / tee -a into the classDENY
git worktree add into the classDENY + redirect to <repo>/.worktrees/
git worktree add outside any .worktrees/ parentDENY + the exact fixed command (Rule 2)
git worktree add whose target can't be resolvedDENY, naming the exact resolution failure (golems#676's requirement, minus the prompt)
Any target the hook cannot read statically, command shows a temp hintDENY — rewrite it with a literal path, or a variable whose value has a literal head
Any target the hook cannot read statically, no temp hintALLOW + advisory (GO-5 E2)
Reads/deletes (ls, cat, grep, rm, worktree list)NEVER denied
Hook/validation error on a payload that mentions a temp locationDENY — fail CLOSED (the S04 half-fire class)
Hook/validation error, no temp hint in the payloadALLOW + advisory (GO-5 E2)
Escape hatch whose ledger can't be writtenDENY (explicit; an unlogged bypass never proceeds)
CLAUDE_WORKERdoes NOT exempt (S04's violator was a worker)
The harness session scratchpadALLOW — the one sanctioned temp location (below)

Path-CLASS (canonicalized via realpath): /tmp, /private/tmp, /var/folders, /private/var/folders, the live $TMPDIR value, and literal $TMPDIR tokens in Bash commands. The macOS /tmp → /private/tmp symlink is not a route-around.

The one exception — the harness session scratchpad (2026-08-17)

Claude Code's own system prompt hands every session a scratchpad and instructs it, verbatim: "IMPORTANT: Always use this scratchpad directory for temporary files instead of /tmp or other system temp directories". The path it supplies is inside the class this guard denies:

/private/tmp/claude-<uid>/<repo-slug>/<session-uuid>/scratchpad/…

So the harness said "put temp files here" and the guard said no. Observed live 2026-08-17: brainlayerClaude took two consecutive denials arming a monitor, and skillcreatorClaude hit the same wall writing a test fixture the same day. Every agent walks into it, because they are following instructions correctly — the same failure shape as the "always notify on commits" flood.

Etan's ruling, 2026-08-17: allowlist the harness session scratchpad, keep denying every other temp path. It is session-scoped and nothing durable belongs there, so allowing it costs nothing Rule 1 was protecting.

The exception is structural and narrow — never a hardcoded uid or session id, and not a widening of the temp path-class. The whole claude-<uid>/<slug>/<session-uuid>/scratchpad chain must sit directly under a temp root, and every component is matched exactly: claude-<digits> (not any directory), a slug, a session UUID, and scratchpad (not scratchpad-evil or myscratchpad). The #727 review found both of those relaxations surviving the suite, so all three are now pinned by deny-side tests that redden the mutation.

It applies to every Rule 1 write surface — Write/Edit, redirects, appends, tee, heredocs — not to redirects only. Still denied: /private/tmp/scratchpad/x.txt (no session chain), /private/tmp/claude-501/x.txt (no scratchpad component), every $(mktemp) form (a bare temp path with no chain at all), /tmp/foo.txt.

.. traversal is defended by the callers, not by the shape match. The path is canonicalized before the shape is consulted: in_temp_class normalizes each candidate (normpath and realpath) and only then asks whether it is scratchpad-shaped, so …/scratchpad/../../../../leak.txt arrives as /private/tmp/leak.txt and denies; _literal_prefix_class rejects a literal .. component outright before building a probe. The normalization inside the shape check itself is defense-in-depth for a direct caller — the #727 review's mutation M5 removed it and opened no hole.

scratchpad is its own prefix-class verdict, decided independently of the repo/outside/temp proofs below.