NAME

App::FuguBench::Hook - the hook verb of fugubench

SYNOPSIS

fugubench [-C <root>] hook SessionStart < payload.json
fugubench hook SessionEnd < payload.json
fugubench hook WorktreeCreate < payload.json
fugubench hook WorktreeRemove < payload.json
fugubench [-C <root>] hook install

DESCRIPTION

App::FuguBench::Hook answers one hook event of Claude Code. The harness writes a JSON payload to standard input, and the verb reads that payload itself, so no hook command needs jq.

The events are SessionStart, SessionEnd, WorktreeCreate, and WorktreeRemove. The word install names the subcommand that writes the entries of those four events into the settings file of the checkout. Another word gives the usage error, and so does an argument after the word.

This verb and the trace verb hold the Claude Code assumptions of the program. Every other verb is agent-agnostic. The payload shape lives here, and in no shared module.

THE PAYLOAD

The verb reads the whole standard input and decodes it as JSON. A payload that is no JSON object gives a warning and the code 0. A payload that holds agent_id stops the verb at once, with no change: that payload belongs to a sub-agent, and an observer dispatches many of them.

The checkout comes from the payload cwd, through the walk to the nearest .toolingrc. A session can start in a worktree, and a worktree is a checkout with its own library clone. The walk cuts no path at a marker.

-C <dir> names the root ahead of the payload. The dispatcher then walks from that value, and a session event still takes its project from the payload cwd.

A session event that finds no checkout warns and returns 0. WorktreeCreate returns 1 and writes no path line.

THE SESSION EVENTS

A session event must never stop a session, so it maps every non-zero code of a call to a warning and returns 0.

SessionStart runs wiki init, and then wiki open. The identifier of the session is session_id, and the verb replaces each character outside a letter, a digit, a dot, a dash, and an underscore with a dash. The page name of wiki open reaches standard output as the only line, and the harness adds that line to the context of the session. wiki init writes the directory of a clone that it makes, and the event runs that call with its standard output on standard error.

The project of the session is the child of the projects directory that holds the cwd, and otherwise the wiki.project value. The projects directory is the wiki.projects value under the home of wiki.origin, as every wiki. value sits under that home. A checkout with no wiki.origin holds no library, and the key has no home there, so the root anchors the value.

SessionEnd runs wiki close with the same identifier. A session that ran no campaign has no page, and that is normal.

THE WORKTREE EVENTS

WorktreeCreate reads name from the payload and runs worktree create. It returns the code of that subcommand, because the harness needs the path of the worktree on standard output. A payload that names no worktree returns 1.

Claude Code runs the create hook again when a session reconnects, with the same name. The subcommand then bootstraps the worktree again, writes the path again, and returns 0.

WorktreeRemove removes nothing. It prints worktree kept: with the path, and the manual command make -C <root> worktree-remove NAME=<name>, to standard error. It returns 0 always, and a payload that names no worktree path gives a warning and the same code.

The verb splits worktree_path at its last worktree.base segment: the part in front is the root, and the part after is the name. That value comes from the checkout of the payload cwd. The split serves the hint alone, and it finds no checkout. Without the segment, and without a checkout that names the value, the verb prints the path alone.

THE INSTALLATION

install writes .claude/settings.json of the checkout: the four entries under hooks, and head at worktree.baseRef. A worktree starts at the local HEAD. Without the hooks, the built-in creation branches from origin/main and skips the bootstrap.

The subcommand answers no event, so it reads no payload, and it takes the checkout of the dispatcher. -C <dir> names the root, and without it the walk starts at the current directory.

Each entry runs the shim of the checkout, "$CLAUDE_PROJECT_DIR/scripts/fugubench" hook <event>, and nothing else. Each one carries an explicit timeout: 120 seconds for WorktreeCreate, 60 for WorktreeRemove, 60 for SessionStart, and 30 for SessionEnd. Without one, the SessionEnd hooks share a budget of 1.5 seconds, and a commit with a push does not fit it.

The writer keeps every key that it does not own, at the top level, under hooks, and under worktree. It replaces the list of each of the four events, and it leaves every other event as it is. The keys reach the file in sorted order, with an indent of two spaces and a final newline, so a second run writes the same bytes and causes no change.

The path of the file, the two keys, and the values live in this module, because Claude Code names them. The doctor takes all of them from here, so the report and the write never disagree.

A file that does not parse is a failure, and the subcommand then writes nothing. A hooks value or a worktree value that is no object is a failure too. The operator repairs the file, and no run of the subcommand destroys it.

command

command($verb) returns the entry of the Fugu::CLI table. The module holds one verb, so it ignores the name.

entries

entries returns a reference to a hash of the four event names. Each value is one list with one matcher-less group, and the group holds one entry. An entry holds type, command, and timeout, and no statusMessage: the harness names the event itself. The doctor reads the same hash, so the report of an entry and the write of an entry never disagree.

worktree

worktree returns a reference to a hash of the worktree settings that install writes. baseRef holds head, because a worktree starts at the local HEAD.

settings_path

settings_path($root) returns the settings file of one checkout root, .claude/settings.json below it.

hooks_key and worktree_key

hooks_key returns hooks, the key that holds the entries of the events. worktree_key returns worktree, the key that holds the worktree settings.

RETURN VALUES

command, entries, and worktree return a hash reference. settings_path, hooks_key, and worktree_key return a string.

The verb returns the exit codes of Fugu::CLI. A session event and WorktreeRemove return 0 after every payload. WorktreeCreate returns the code of worktree create, and 1 when the payload names no worktree, when it names no cwd, and when no .toolingrc sits above that cwd. An unknown event, and an argument after an event, give the usage error and return 2.

install returns 0. It returns 1 when the settings file does not read, when it does not parse, when a key of it holds no object, and when the write fails. It returns 3 when no .toolingrc sits above the start of the walk.

SEE ALSO

App::FuguBench, App::FuguBench::Checkout, App::FuguBench::Wiki, App::FuguBench::Worktree, Fugu::CLI, Fugu::File

AUTHORS

Dick Olsson <hi@senzilla.io>