hook¶
Serve another tool's hooks for the workspace a session starts in.
A hook command reads the calling tool's JSON payload on standard input, does
the work that tool asked for, and answers on standard output in the shape the
tool documents. Today one family is served: gwz hook claude-code, which
carries the two hooks themselves and the setup command that installs them.
gwz hook claude-code worktree-create¶
Reads Claude Code's WorktreeCreate payload and prints the path of the working
copy the session should use. One test decides what it makes:
- A GWZ workspace gets a local clone of the whole workspace at
../<root-dirname>-<name>, registered in the local clone family. When the session started inside a member, the printed path is that member's directory inside the lane. - Anything else gets the worktree Claude Code would have made itself: a
git worktreeunder.claude/worktrees/<name>on branchworktree-<name>, fromorigin/<default-branch>when it resolves andHEADotherwise, with the.worktreeincludecopy performed for a worktree the hook created. The patterns are matched against the project root's own untracked files, by their path relative to that root;<root>/.claude/worktrees/and the project's.gitare outside the enumeration, so one worktree's included files are never copied into another.
The contract is narrow on purpose: the hook prints only a path it created or
verified in this invocation, and nothing else ever reaches standard output.
Every failure is one line on standard error, gwz: <cause>; <remedy>, with a
non-zero exit and an empty standard output, which makes Claude Code abort the
creation.
Reuse¶
An existing lane of the same name is reused only when all of these hold: the
family index has a ready row for the name, its recorded path is the
destination, the row belongs to this session, and the lane passes the
completeness check. Every other state is refused, naming the state and its
remedy — an unfinished create, a lane at another path, a lane owned by another
session, a lane that is incomplete.
The completeness check is cheap on purpose, because a reuse has to stay a
reuse: gwz's own observation of the lane (the pointer and the allocation
marker, which is what gwz local list reports), and then, for each
repository the workspace lists, that its directory is there in the lane and
holds a .git. It is not a tree walk, a status or a fetch, and it writes
nothing. A lane that fails it is refused, named, and left for the retirement
procedure.
Guards¶
Two guards protect the copy, and neither applies to a reuse:
- Free space on the filesystem holding the destination's parent, against a
run-time estimate of what the copy costs: a walk of the source for apparent
size and file count, a block-sharing probe at the destination's parent, and a
pessimistic share by filesystem.
--min-free-gbis a floor applied on top of the estimate. --max-lanes(default 8) ready rows in the family.
gwz hook claude-code worktree-remove¶
Reads the WorktreeRemove payload, canonicalises worktree_path and
classifies it. A registered git worktree of the project is removed with
git worktree remove, never --force. A lane root, or a member directory
inside one, is disposed with gwz local dispose from the family root, never
--keep and never --force: a hazard refusal keeps the lane and the session.
That refusal is one line — how many hazards there are, across how many
repositories, and of which class — because the report itself belongs to
gwz local dispose, which the remedy names. A path that no longer exists
exits zero and is logged as already-absent. Anything else is refused.
Options¶
Every knob is an option on the handler itself, because the desktop app passes no environment variable of ours. Each leaf carries only the options it obeys: removal consumes nothing, so none of the creation guards appear on it.
| Option | Meaning | Leaf |
|---|---|---|
--min-free-gb <gb> |
A floor on free space, on top of the estimate (default: no floor; only the estimate applies) | create |
--max-lanes <n> |
Ready-lane ceiling (default 8) | create |
--wait-secs <secs> |
Deadline for the attempt loop, and for a removal's family lock (default 300, which setup raises to the estimated copy time when that is longer) |
create, remove |
--base-ref <ref> |
Base for the fallback worktree (default origin/<default-branch>, else HEAD) |
create |
--log <path> |
Log location, instead of the fixed one | create, remove |
setup takes the creation options and bakes them into the handler text; the
WorktreeRemove handler it writes carries only --wait-secs and --log.
The log¶
Each hook writes one line per decision: to <root>/.gwz/claude-hooks.log in a
workspace and ~/.claude/gwz-lane-hooks.log otherwise, or wherever --log
names. A line carries a timestamp, the event, the name, the session id, the
classification (lane, member-in-lane, fallback-worktree,
already-absent, refused-by-hook, refused-by-hazard, family-busy), the
path, the outcome and the exit code — never the transcript path and never the
working directory. The name is the lane's, which on a removal the hook reads
off the family row, because Claude's removal payload carries no name; the
path is the one the hook printed or acted on, including on a refusal. A location that would appear in
git status is not used; the user-level log is used instead and a note says
so. The file is bounded at 1 MB and truncated to its newest half beyond that.
gwz hook claude-code setup¶
Prints the hooks block, and with --write merges it into a settings file or
with --remove takes it back out. A placement flag is mandatory:
gwz hook claude-code setup --project # print the block
gwz hook claude-code setup --project --local --write # merge into settings.local.json
gwz hook claude-code setup --user --write # merge into ~/.claude/settings.json
gwz hook claude-code setup --project --local --remove # take it back out
The block carries one WorktreeCreate handler and one WorktreeRemove
handler, each with its own timeout. Inside a GWZ workspace the timeouts and
the handlers' --wait-secs are computed from the same run-time estimate the
create hook uses: the wait is the compiled-in 300 s, which the estimated copy
time can raise but never lower, and the timeouts are one wait plus one
estimated copy plus 60 s for creation and one wait plus 60 s for removal.
Outside a workspace — including in a plain repository, where the block is
still written for the D8 fallback — no estimate is run and both are the
compiled-in defaults: a 300 s wait and 600 s timeouts.
The default handler is the bare command gwz hook ..., resolved through
PATH, so a committed project block is machine-independent and identical
everywhere, which is what Claude Code's same-handler dedupe keys on.
--command PATH pins an absolute binary instead, for a machine whose desktop
app cannot see gwz on its PATH.
setup also reminds you to install or refresh the agent skill: copy
skills/gwz/SKILL.md to ~/.claude/skills/gwz/.
Getting a session into a lane¶
After --write, run claude --worktree <name> from the workspace root (or
start a background session). The lane appears in gwz local list with the
session id as its owner. Integrate it with
gwz --target @all merge --remote <name> from the main workspace, then
gwz local dispose <name>. See Claude Code for the whole
lifecycle.
Writing and removing¶
--write and --remove are the lifecycle pair, and are refused together.
Both edit another program's configuration, so each:
- parses the existing file first and refuses one that does not parse, is a symbolic link, or is not a regular file;
- writes a temporary file beside the target, fsyncs it, re-parses it and renames it over the original;
- changes no byte outside the block it inserts or the entries it removes.
--write in addition creates the file when it is absent and does nothing when
the block is already there. --remove takes out the two entries this tool
wrote, drops a WorktreeCreate or WorktreeRemove array or a hooks object
left empty behind them, never deletes the file itself, and says so and changes
nothing when the file does not carry the block. A removal after a write leaves
the file byte for byte as the write found it.
Placements¶
One placement is the recommendation. A block may sit in both a project's
settings and a user's: Claude Code runs an identical handler once, so identical
text is harmless. Two differing handlers both run, in parallel, for the same
name and session; setup warns, naming both files, when it finds one. They
stay harmless for creation — the second handler reuses the first's lane and
prints the same path — but a handler that refuses for a reason of its own makes
Claude abort the creation the other completed, and that lane is an orphan whose
inventory is gwz local list.
See Claude Code for the guide.