agentleFS
EtanHeyEtanHey/golems/skills/golem-powers/tmp-block/SKILL.md

tmp-block

Skill0 starsChanged 15 days agoApache-2.0View on GitHub

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.

The file

We split this skill into the rules it gives an agent

2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Ask the lead to reinstall the hooks." Recovery uses !

    line 22
  2. Very specific:

    Every use is logged to the durable ledger ~/.claude/logs/tmp-block-ledger.jsonl (override: TMP_BLOCK_LEDGER).

    line 221

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

tmp-block — durable staging only, NEVER /tmp

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 the plain refusal: "Blocked: a golems safety check couldn't run. Ask the lead to reinstall the hooks." Recovery uses ! bash ~/Gits/golems/scripts/hooks/install-hooks.sh --host <host> --update --apply. 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.

Rule 2 — the worktree location convention (2026-08-09)

Ratified by Etan by voice, 2026-08-09: the fleet worktree location is the in-repo <repo>/.worktrees/<name>.

The gap this closes is Etan's own catch, verbatim: "I thought the guard's job was to guard it, so it actually goes the right way." Rule 1's deny message advised <repo>/.worktrees/ — but nothing enforced it, so git worktree add $HOME/Gits/foo.wt/bar (durable, not temp) sailed straight through and 18 sibling *.wt directories accumulated under $HOME/Gits/.

Resolved targetDecision
has a .worktrees ancestor componentallow
resolves anywhere elseDENY, naming the convention + git -C <repo> worktree add <repo>/.worktrees/<name>
cannot be resolved ($UNSET, $(…), relative after cd/git -C)DENY, naming the resolution failure

Rule 2 silently allows a target whose literal or already-resolvable, repo-anchored prefix proves .worktrees/ containment before an unresolved variable suffix, such as git worktree add .worktrees/$wt or $HOME/Gits/golems/.worktrees/$wt. An unresolvable variable prefix still asks; a literal .. escape still disqualifies the proof; and lexical plus realpath containment checks still reject a .worktrees symlink that leaves the repo.

golems#676 applied. git-guardian judged the unexpanded literal — it blocked a legitimate cleanup of a mktemp -d variable, then blocked the gh issue create that filed the bug because the title carried the pattern as prose. So Rule 2: expands ~/$NAME from the live environment and judges the resolved path (lexical and realpath, so a .worktrees symlink pointing at a sibling is not a route-around); fires only in command position (quoted text and heredoc bodies run nothing); and names the exact resolution failure instead of blocking blind when the target is unknowable. #676's requirement was the actionable reason, not the prompt — the prompt was dropped on 2026-08-17 and the reason kept.

Migration-window hatch, mirroring Rule 1: WEAVE_ALLOW_WT_MIGRATION=1 — allowed AND logged. It is location-scoped only; it never unlocks the temp class (that still needs WEAVE_ALLOW_TMP).

This is the backstop, not the only fix. It sees git worktree add issued as a Bash tool call. Generators that create worktrees in-process never reach it — cmuxlayer/src/worktree.ts:240 builds join(homeGitsDir, ${repo}.wt , spec.name), which is where the 18 dirs actually came from, and is fixed in the cmuxlayer/golems lanes. Likewise a worktree add nested in a quoted payload (bash -c "git worktree add …", an agent-spawn prompt string) is one token to this parser and is not caught.

Escape hatch = bypass-detector seed

Over-broad guards INDUCE route-arounds (adversary Attack 5.4), so genuinely-ephemeral writes have a sanctioned path:

WEAVE_ALLOW_TMP=1 <command>              # temp path-class (Rule 1)
WEAVE_ALLOW_WT_MIGRATION=1 <command>     # worktree location (Rule 2)

Both are inline-prefix or session-exported, and both are scoped to their own Bash simple command when used inline.

Every use is logged to the durable ledger ~/.claude/logs/tmp-block-ledger.jsonl (override: TMP_BLOCK_LEDGER). If the ledger cannot be written, the bypass is DENIED — an unlogged bypass must not proceed. Audit the ledger during weaves: entries that look durable (.md notes, worksheets, worktrees) are route-arounds to flag.

The three class proofs (temp / repo / outside)

A target with an unreadable suffix is still judged by its literal head, because every value it can take starts with that head. The head proves one of three classes: temp (deny), repo (allow), or — since 2026-08-17 — outside, an absolute head that is provably not temp and belongs to no repository. outside is what makes > ~/Documents/probe_$$.txt allow instead of stranding a pane; without it the guard could prove only two classes and had to ask about the entire home directory.

The head is read through variables too: P=~/Documents/x_$$.txt; … > "$P" is judged exactly like the same path spelled inline, and P=/private/tmp/x_$$.txt still reaches the hard temp-class deny. A variable with no literal head (P=$UNSET/x, P=$(mktemp)) proves nothing and is refused. Rule 2 does not consume outside: a worktree target must still prove repo membership.

Known limits (the detection frontier)

Statically uncaught: inline-interpreter writes (python3 -c "open('/tmp/…')"), cp/mv/rsync into the class, relative paths from a temp cwd.

The guard is no longer Claude-only (corrected 2026-08-19). The 2026-08-17 note here said neither Codex nor Cursor had a hook configured and both reached /tmp freely. Measurement says otherwise on both halves; the findings are summarized in Cross-agent coverage below, and the raw probe record is kept out of git in docs.local/plan/tmpblock-cross-agent/MEASUREMENTS.md.

The literal-head residual (#703, widened 2026-08-17; narrowed 2026-08-20). The suffix a head proof cannot read can still affect the final path. A literal .. inside a command-substitution body is now disqualifying, just as it is in the outer target, so > /durable/head$(printf /../../tmp/leak) denies while a non-traversing suffix such as $(date +%s) remains allowed. Measured history:

head95964ba4now
repo-contained traversal — <repo>/docs.local$(printf /../../tmp/x)allowdeny
non-repo traversal — /workspace/golems$(printf /../../tmp/x)askdeny
non-traversing suffix — <repo>/docs.local/x-$(date +%s).logallowallow

The remaining accepted residual is a dynamic suffix whose output cannot be read and whose command text contains no literal parent traversal. The discriminator for this closure is the visible .. component, not repo-containment or whether a / separates the head from the substitution. Both deny and allow sides are pinned beside each other in tests/test_worktree_anchor.py.

The ledger + a periodic temp-dir scan for agent-shaped artifacts are the detectors for this whole frontier — detectors trail the evasion frontier by construction.

Cross-agent coverage (measured 2026-08-19)

The hook is a Claude-format PreToolUse script, and two of the three fleet harnesses read that format. What differs is the tool name each one sends, which is why "the hook is installed" and "the hook is enforcing" were not the same sentence.

HarnessLoads this hook fromShell arrives asFile write arrives asStatus
Claude Code~/.claude/settings.jsonBashWrite/Edit/NotebookEditenforcing since 2026-06-07
Cursor~/.claude/settings.json — read unconditionally, no opt-inShellWriteWrite was already enforcing; Shell was a silent no-op until this change
Codex~/.codex/hooks.jsonBashapply_patchhooks-live installer supports wiring; verify each installed seat

Cursor was never unguarded, and never fully guarded. Its bundle (2026.08.11-e8db854) carries a literal claudeUserConfigPath pointing at ~/.claude/settings.json, and maps Claude matchers through {Bash: "Shell", Edit: "Write", …}. A live probe with no local tap got the verbatim ⛔ TMP-BLOCK reason back for Write(/tmp/…) and the file was never created — so that half has been enforcing all along. A shell redirect, arriving as tool_name: "Shell", missed the tool_name != "Bash" gate and was allowed with exit 0. TOOL_ALIASES closes that.

Codex has a real hook surface, and :workspace is not a substitute for it. default_permissions = ":workspace" does not deny /tmp — Codex prints its own writable roots at startup, and they are workspace-write [workdir, /tmp, $TMPDIR]. The sandbox does enforce (a write to ~/Documents under codex sandbox returns Operation not permitted), so /tmp is granted on purpose, not by omission. Two surfaces can close it:

  • the hook — ~/.codex/hooks.json, matcher: "Bash|apply_patch", enabled by default; this guard now speaks its dialect (hookSpecificOutput.permissionDecision) and handles its apply_patch envelope.
  • the sandbox — [sandbox_workspace_write] exclude_slash_tmp / exclude_tmpdir_env_var, or a [permissions] profile with a deny on the slash_tmp / tmpdir special paths. This is stronger than the hook: it catches python3 -c, cp, mv and relative-from-temp-cwd — the entire detection frontier above — because it is a kernel-level sandbox rather than a static parse.

Codex uses a transport adapter, not the Claude hook command directly. Codex 0.160.0 ignores stdout JSON at exit 2 unless there is a blocking reason on stderr. scripts/hooks/codex-policy-hook.py runs this existing gate and git-guardian, translates deliberate denials into exit-0 Codex JSON, and denies transport failures with a static repair hint. The registered shell fallback uses stderr + exit 2 if the interpreter or adapter cannot start.

scripts/hooks/install-hooks.sh --host <mbp|m1> --update --apply registers both gates from the selected pinned manifest into $CODEX_HOME/hooks.json (default ~/.codex/hooks.json), backs up changed hook configuration, and preserves config.toml. It never creates trust hashes or overrides an existing disabled-hooks setting. Review /hooks from plain codex with no --profile so trust persists in base config.toml. repoGolem's per-launch profile is deleted on exit, taking trust given there with it. --apply prints this instruction; --status reports wiring and base-config trust as missing/disabled/present-unverified, with nonzero status for missing/disabled trust. A stored hash needs native review; positional hook keys change when groups are reordered. Profile/CLI overrides and managed requirements can change effective enablement and are outside this base-config status check.

A synthetic local Responses fixture exercises the real 0.160 CLI, including denial before execution and permitted commands. It does not prove an installed authenticated fleet seat; the lead must run that proof after deployment. Outer hook timeouts and skipped/untrusted hooks remain runtime limits. Codex omits per-call workdir from Bash hook input: a relative write executed in a temp-class workdir can pass when session cwd is ordinary. write_stdin into a persistent shell has no hook payload, so a force push typed there is unhooked. Both holes are proven on real 0.160; this adapter cannot recover missing context or intercept that input. Seven-second policy checks and deduplicated patch paths stay within the ten-second native timeout; large/timed-out calls ask to split the patch. Indented patch headers use the shared parser, and temp deletes remain allowed. See version-pinned research.

Install & evals

  • Install: hooks/INSTALL.md (symlink + settings.json PreToolUse entry).
  • Evals: hooks/tests/test_tmp_block.py (280 cases, incl. the verbatim S04 replay), tests/test_worktree_anchor.py (78 cases, static anchor and literal-head unit coverage), and hooks/tests/test_cross_agent.py (26 cases, the measured Cursor and Codex payload shapes and the refusal dialect) — 384 together. Rule 1: RED against the prior guard stack (15/17 pass-through), GREEN. Rule 2: RED 12 failing / 51 passing against the advise-only guard (every enforcement case exited 0 with {}; the .worktrees/ and prose cases were already silent), GREEN. Run both: python3 -m pytest hooks/tests/ tests/ -q.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.