· about 35 minutes · runs on ubuntu-26.04-claude · unrated
An unrated lab. It runs on your machine with no account and no network, and everything about it — the faults, the checks, the hints and the reference solution — is in the repository. An attempt on it is recorded on your profile and never moves a rating: only rated labs do. Rated and unrated labs.
The billing team lets a nightly Claude Code job, ~/bin/nightly-cleanup, tidy the code in
~/billing. Three hooks in the repository's .claude/settings.json are the guard rails everyone
agreed on: Python the agent writes is re-indented with spaces (the model likes tabs), the agent never pushes,
and migrations that have already run against production are never edited.
Last night the agent edited an applied migration, committed it and pushed it to origin — and the
Python file it wrote is indented with tabs. Nobody changed the hooks. The job's JSON result
is in ~/nightly-cleanup.log.
This lab runs in a container, as learner with sudo. claude here is the real Claude Code
2.1.270, talking to a scripted model on this machine: every run replays last night's plan, and
Claude Code carries out the tool calls — and runs the hooks — for real. origin is a bare
repository at /srv/git/billing.git.
What is expected, and graded — the grader runs ~/bin/nightly-cleanup itself, against models that
try other things:
git status.origin.Keep the hooks: the job's own permissions are not the problem to solve here.
This lab runs in a container — Docker or Podman, no VM — so there is no boot to survive: the checks run once, against the state you left.
| Check | Objective |
|---|---|
| 01_written_code_gets_tidied | Make a PostToolUse hook run on the tools it is meant for, with the input Claude Code gives it |
| 02_agents_cannot_push | Make PreToolUse hooks actually block what they are written to block |
| 03_applied_migrations_are_protected | Make PreToolUse hooks actually block what they are written to block |
| 04_the_applied_migration_is_restored | Undo what an agent pushed, the way a person would |
Where the lab's hints send you, level by level, as you ask for them (h, then l opens a journal section in the TUI).
4 questions on the same topic, in the lab's Theory tab. They never affect the lab's grade. Three of them, to answer here:
A PreToolUse hook has `"matcher": "bash"`. The model runs `git push` through the Bash tool. Does the hook run?
No: a matcher of plain letters is an exact, case-sensitive tool name, and the tool is Bash
A matcher made only of letters, digits, _, -, commas, | and spaces is a list of exact tool names, and names are case-sensitive: `bash` matches nothing. Anything with other characters is an unanchored JavaScript regex, and even then `bash` would not match `Bash`. -p runs hooks the same way as an interactive session. Verified with Claude Code 2.1.270: a `bash` matcher never fired, and neither did `edit|write` for the Write tool.
https://code.claude.com/docs/en/hooks
A PreToolUse hook for Bash detects `git push`, prints "Blocked: agents do not push" and exits with status 1. What happens to the push?
It runs: exit 1 is a non-blocking error, reported but not enforced
Only exit status 2 is a blocking error for PreToolUse: the call is refused and stderr is given to the model as the reason. Status 0 is success, and any other status is a non-blocking error — the action proceeds. The run does not stop. To block, exit 2 with the reason on stderr, or exit 0 with JSON whose hookSpecificOutput.permissionDecision is "deny".
https://code.claude.com/docs/en/hooks
A PostToolUse hook for Edit|Write is `file="$1"; expand -i -t 4 "$file" ...`. The hook runs (a trace line proves it), yet no file is ever re-indented. Why?
Command hooks get their input as JSON on stdin; $1 is empty, the file path is tool_input.file_path
Claude Code starts a command hook with no arguments and writes a JSON object to its stdin: hook_event_name, tool_name, tool_input (for Write: file_path and content), session_id, cwd and more. Read it with jq: `jq -r .tool_input.file_path`. PostToolUse runs after the tool succeeded, so the file exists, and hooks run with the user's own permissions — no sandbox.
https://code.claude.com/docs/en/hooks