Problem
worklog's premise is that the live session supplies the why a PR body can't — the decisions, the
dead ends, the thing that cost an hour. That value is highest in exactly the sessions where you
forget to run it. Run it the next morning from a fresh chat and all it can draw on is the merged-PR
scan, which is the part you didn't need help remembering.
There's no way to fix this from inside the skill: a skill is inert until something invokes it, and
the failure mode is nothing invoking it.
Proposal
Ship an optional, default-off hook pair in the journal plugin that fires worklog in the
session that did the work, triggered on PR creation.
worklog-arm.sh — PostToolUse / Bash. When a command runs gh pr create in command
position and the output carries a /pull/ URL, write a sentinel at
~/.claude/worklog-pending/<session_id> (line 1 = cwd, line 2 = PR URL).
worklog-stop.sh — Stop. If the sentinel exists, delete it and return
{"decision": "block", "reason": …} instructing the session to invoke journal:worklog before the
turn ends.
Stop + decision: "block" is the load-bearing primitive here, and worth stating plainly: it is the
only mechanism that runs something in the originating session's context. SessionEnd, a cron, or
an external watcher can all notice that a PR was opened, but each can only act from a fresh context —
which is precisely the failure being fixed.
Deleting the sentinel before emitting the block is what makes it strictly one-shot: a worklog that
fails, or that the user declines, cannot re-block or loop. stop_hook_active is a second guard.
Packaging (verified against Claude Code 2.1.219)
A plugin's hooks/hooks.json is auto-loaded when the plugin is enabled, and ${CLAUDE_PLUGIN_ROOT}
is substituted per-element as a plain string rather than through a shell parser (so paths with
spaces/quotes are safe). So plugins/journal/hooks/{hooks.json,worklog-arm.sh,worklog-stop.sh}
requires no install instructions beyond enabling the plugin — no manual settings.json surgery.
Design constraints
- Default off, opt in via config. Gate on a user-level key —
journal.autoWorklogOnPR in
~/.claude/maintainerd.json — checked by the arm script, which no-ops when absent. "Optional"
should mean a config line, not a separate plugin. Enabling journal to get the /worklog command
must never silently start blocking end-of-turns. This also keeps the opt-in in the same
config-contract idiom as everything else in the marketplace, rather than inventing a new one.
- Fail open, always. Missing
jq, unparseable payload, unwritable state dir → exit 0,
silently. A journaling convenience must never be able to wedge a turn.
- Dependency surface. The reference implementation assumes
bash + jq + grep -E. The repo
currently assumes none of those. Either declare the dependency and degrade cleanly when it's
missing, or drop jq.
- State hygiene. Sentinels are only consumed by the
Stop hook. A session that opens a PR and is
then killed or /cleared leaves the file behind forever. Needs an age-based prune or a
SessionStart sweep.
Why this deserves more scrutiny than a skill PR
maintainerd currently ships 20 skills across 7 plugins and zero lines of executable code. All of
it is markdown that stays inert until a model chooses to read it. A hook is categorically different:
it runs on every Bash tool call, in every session, for everyone with the plugin enabled, with neither
a model nor a user deciding anything in the moment.
Concretely, from building the local version: the first draft matched gh pr create anywhere in the
command string, and armed a session that had created no PR — the test fixtures being echoed contained
both the command text and a /pull/ URL. The fix is to require command position (start of line, or
after ; && || pipe subshell), but the general lesson is that a hook keyed on command text must
distinguish invoking from quoting, and there is no test harness in this repo that would have caught
it. Adding one is arguably a prerequisite.
Related: settings/plugin hooks hot-reload mid-session, so a half-finished hook is immediately
load-bearing on the machine editing it.
Sequencing
Do not ship yet. The local version has only been exercised against synthetic payloads (7-case
table on the arm hook: bare invocation / after && / second line → arms; quoted in a fixture /
greping for the string / failed create / unrelated command → clean; plus the Stop hook blocking
once then going silent). The single live firing to date was the false positive described above.
Run it on real PRs for a week or two first, then port.
Acceptance criteria
Non-goals
- Generalizing the arm/consume pattern into shared "end-of-session trigger" machinery for other
skills. Tempting, but there is exactly one instance of it.
- Triggering on anything other than PR creation (merge, session end, archive). For the record: there
is no archive-a-chat hook in Claude Code — verified against 2.1.219, where the full event list is
PreToolUse, PostToolUse, PostToolUseFailure, UserPromptSubmit, Notification, Stop,
SubagentStart, SubagentStop, PreCompact, PostCompact, SessionStart, SessionEnd.
Reference implementation currently lives in ~/.claude/hooks/ on my machine; context and the full
reasoning are in the 2026-07-27 dev log.
Problem
worklog's premise is that the live session supplies the why a PR body can't — the decisions, thedead ends, the thing that cost an hour. That value is highest in exactly the sessions where you
forget to run it. Run it the next morning from a fresh chat and all it can draw on is the merged-PR
scan, which is the part you didn't need help remembering.
There's no way to fix this from inside the skill: a skill is inert until something invokes it, and
the failure mode is nothing invoking it.
Proposal
Ship an optional, default-off hook pair in the
journalplugin that firesworklogin thesession that did the work, triggered on PR creation.
worklog-arm.sh—PostToolUse/Bash. When a command runsgh pr createin commandposition and the output carries a
/pull/URL, write a sentinel at~/.claude/worklog-pending/<session_id>(line 1 = cwd, line 2 = PR URL).worklog-stop.sh—Stop. If the sentinel exists, delete it and return{"decision": "block", "reason": …}instructing the session to invokejournal:worklogbefore theturn ends.
Stop+decision: "block"is the load-bearing primitive here, and worth stating plainly: it is theonly mechanism that runs something in the originating session's context.
SessionEnd, a cron, oran external watcher can all notice that a PR was opened, but each can only act from a fresh context —
which is precisely the failure being fixed.
Deleting the sentinel before emitting the block is what makes it strictly one-shot: a worklog that
fails, or that the user declines, cannot re-block or loop.
stop_hook_activeis a second guard.Packaging (verified against Claude Code 2.1.219)
A plugin's
hooks/hooks.jsonis auto-loaded when the plugin is enabled, and${CLAUDE_PLUGIN_ROOT}is substituted per-element as a plain string rather than through a shell parser (so paths with
spaces/quotes are safe). So
plugins/journal/hooks/{hooks.json,worklog-arm.sh,worklog-stop.sh}requires no install instructions beyond enabling the plugin — no manual
settings.jsonsurgery.Design constraints
journal.autoWorklogOnPRin~/.claude/maintainerd.json— checked by the arm script, which no-ops when absent. "Optional"should mean a config line, not a separate plugin. Enabling
journalto get the/worklogcommandmust never silently start blocking end-of-turns. This also keeps the opt-in in the same
config-contract idiom as everything else in the marketplace, rather than inventing a new one.
jq, unparseable payload, unwritable state dir →exit 0,silently. A journaling convenience must never be able to wedge a turn.
bash+jq+grep -E. The repocurrently assumes none of those. Either declare the dependency and degrade cleanly when it's
missing, or drop
jq.Stophook. A session that opens a PR and isthen killed or
/cleared leaves the file behind forever. Needs an age-based prune or aSessionStartsweep.Why this deserves more scrutiny than a skill PR
maintainerd currently ships 20 skills across 7 plugins and zero lines of executable code. All of
it is markdown that stays inert until a model chooses to read it. A hook is categorically different:
it runs on every Bash tool call, in every session, for everyone with the plugin enabled, with neither
a model nor a user deciding anything in the moment.
Concretely, from building the local version: the first draft matched
gh pr createanywhere in thecommand string, and armed a session that had created no PR — the test fixtures being echoed contained
both the command text and a
/pull/URL. The fix is to require command position (start of line, orafter
;&&||pipe subshell), but the general lesson is that a hook keyed on command text mustdistinguish invoking from quoting, and there is no test harness in this repo that would have caught
it. Adding one is arguably a prerequisite.
Related: settings/plugin hooks hot-reload mid-session, so a half-finished hook is immediately
load-bearing on the machine editing it.
Sequencing
Do not ship yet. The local version has only been exercised against synthetic payloads (7-case
table on the arm hook: bare invocation / after
&&/ second line → arms; quoted in a fixture /greping for the string / failed create / unrelated command → clean; plus theStophook blockingonce then going silent). The single live firing to date was the false positive described above.
Run it on real PRs for a week or two first, then port.
Acceptance criteria
plugins/journal/hooks/{hooks.json,worklog-arm.sh,worklog-stop.sh}, wired via${CLAUDE_PLUGIN_ROOT}journal.autoWorklogOnPRis set in the user-level~/.claude/maintainerd.jsonstop_hook_activerespectedjournalplugin README / roadmap documents the opt-in, the dependencies, and how to turn it offjournal.autoWorklogOnPRNon-goals
skills. Tempting, but there is exactly one instance of it.
is no archive-a-chat hook in Claude Code — verified against 2.1.219, where the full event list is
PreToolUse,PostToolUse,PostToolUseFailure,UserPromptSubmit,Notification,Stop,SubagentStart,SubagentStop,PreCompact,PostCompact,SessionStart,SessionEnd.Reference implementation currently lives in
~/.claude/hooks/on my machine; context and the fullreasoning are in the 2026-07-27 dev log.