# /autoplan publication guard: troubleshooting `/autoplan` installs a `PreToolUse` hook (`autoplan/bin/phase-publication-hook`) at its phase boundaries. Before it lets a phase-entry `Read` or a reviewer `Agent` dispatch run, it reads this session's own Claude Code journal (`~/.claude/projects//.jsonl`) and checks that the previous phase's report was published as parent assistant text. Every guard message names a **code**, the Claude Code version that wrote the journal, and a link to the section below for that code. Include the code and version when you report a problem. The guard's decision log (see [The decision log](#the-decision-log)) holds the same information. Each code has one of four dispositions: | Disposition | What it means | What you do | |-------------|---------------|-------------| | Unverified allow | Claude Code left this call uncheckable. The call runs, with a visible warning. | Nothing, unless it keeps happening (then report it). | | Fallback | Retrying cannot help. | Run the fallback below. | | Corrective | One named action must happen first. | Take that action. | | Transient | Something is still in progress. | Wait for the named thing, then continue. | **The fallback** (printed in every fallback denial): 1. Run the reviews by hand: `/plan-ceo-review`, then `/plan-devex-review`, then `/plan-eng-review`. 2. Or move to a fresh session and keep your work: `/context-save`, start a new Claude Code session in the project (not `--resume`), `/context-restore`, then `/autoplan `. There is no environment variable that turns the guard off. ## How a phase transition is published Claude Code can write a tool call to the journal after the hook has already run, and text in the same assistant message as a tool call can be invisible to the hook. So the guard counts a phase report only when a later journaled record follows it: a tool result, or a record from another assistant message. The phase-close procedure therefore publishes each report in a message whose only tool call is the Bash no-op `true autoplan-published ` (it prints nothing and needs no permission; Claude Code 2.1.292 asks for approval of a bare `:` command, so the no-op is `true`), and makes the next phase's `Read` or `Agent` call in a later message. Sessions that put the report and the next phase's call in one message get one [`publication_unflushed`](#publication-unflushed) denial naming that fix. When Claude Code has not yet written the current call, the guard evaluates the call from the hook payload instead (Claude Code's own parsed input), placed after the last journaled record. The previous phase's report must still be in the journal. ## Unverified allows These states are caused by Claude Code, not by the model. The guard lets the call run without a permission decision (Claude Code's own permission check still runs), prints a warning to you that names the code, and logs the allow with `disposition: "unverified"`. For that one call it does **not** check that the previous phase's report was published. > [autoplan] phase publication was NOT verified for this session (journal_lag): ... ### `unrecognized_shape:` The journal root has a shape the guard does not know, usually because Claude Code changed its journal format. The guard reads the journal twice and allows only when both reads agree. `unrecognized_shape:cwd_spelling` means the journal's directory is the same folder as the project, spelled differently. `unrecognized_shape:compaction_unreachable` means records after a Claude Code compaction hang from a compact boundary whose logical parent loops back instead of reaching the session root, in a layout gstack does not read yet (Claude Code 2.1.300's preserved segment is read). Earlier gstack versions dropped those records and denied the next phase with `close_incomplete`. Please report the detail and version. ### `journal_lag` Claude Code had not written the previous assistant message to the journal (a tool in an earlier message still has no result, or an ancestor record is missing) and did not catch up within 2 seconds. ### `rewritten` The journal bytes the guard had already read changed during one hook call: a later read's sha256 of bytes [0, first size) differs from the first read's, or a record pass two reloads no longer parses to the record pass one indexed. Claude Code normally only appends, and appends never fail a read. Rewrites between hook calls are not detected (an accepted limit). ### `too_large` A single journal record is over the 32 MiB record limit (`OWNED_RECORD_MAX_BYTES`). The guard reads long journals record by record, so a large journal alone never triggers this; one enormous record (for example a huge pasted file) does. A record still being written (no newline yet) is not read at all. ### `unseen_version` A Claude Code release newer than the one gstack has checked passes the hook a reviewer dispatch without a key the journal has (a schema strip). The daily `autoplan-schema-canary` case catches these within a day of a release. ## Fallback denials ### `competing_root` The journal has more than one conversation root, or two records share one UUID. ### `foreign_cwd` The journal's root was written in another project directory, after spelling and symlinks are taken into account. Linked git worktrees of the project are accepted. ### `sidechain` The first turn or its ancestry is a sidechain (subagent) record, not the parent session. ### `agent` The conversation ancestry passes through a subagent record. ### `cycle` The journal's parent links form a loop. ### `identity` The journal path, its directories or the file failed the identity checks: a symlink, a foreign session, the wrong directory layout, a different file, or a journal that shrank. A rerun hits the same check. ### `oversized_invocation` The current `/autoplan` invocation needs more journal records read in full than the 32 MiB retained-data bound (`OWNED_RETAINED_MAX_BYTES`). The guard indexes every record without its content and reloads in full only what it evaluates: snapshot init results, guarded and close-packet Reads, structured Bash results, error results, assistant text after the first close-packet Read and edits of the active plan. A rerun reads the same records; use the fallback. ### `malformed` A complete journal line is not valid JSON or UTF-8, or its records contradict each other. ### `journal_missing` The session journal named by the hook does not exist after 2 seconds. ### `event_order` The journal's events could not be put in a consistent order for this session. ### `tool_identity` Two journal records claim the same tool call identity. ### `current_mismatch` The journal records the current call with a different tool or input than Claude Code passed to the hook. Only `run_in_background` (which Claude Code's fork-subagent schema drops) and the spelling of `file_path` may differ. ### `agent_key` The reviewer dispatch carries a key outside `prompt`, `description`, `subagent_type` (`general-purpose` only) and `run_in_background`. A model override is not allowed for /autoplan reviewer dispatch. The log records the key names, never their values. ### `cross_phase_batch` One assistant message enters two different phases. Calls that enter the same phase may share a message; another phase needs its own message after the previous report. ### `init_mismatch` Snapshot init returned paths that do not match the canonical form of its own command arguments, or its artifacts (restore point, active plan header) do not match this invocation. The guard canonicalizes the `` and `` arguments the way `init` does (see [Symlinked plan directories](#symlinked-plan-directories)) before comparing, so a retargeted or forged init result is denied here. Zero results or duplicate results for one init use also deny as `init_mismatch`. Do not re-run the same call: run init with the paths the guard expects. ### `snapshot` A phase snapshot, methodology or other immutable artifact is missing, aliased, writable or changed. ### `dispatch_prompt` The reviewer prompt differs from the snapshot's exact `nativeDispatchPrompt`. ### `evidence` An artifact the guard needed could not be parsed. ### `publication_repeat` The report was published separately once in this invocation and still cannot be verified, so Claude Code is not writing it to the journal in time. ## Corrective denials ### `publication_missing` No filled report for the previous phase appears after its close packet Read. Publish it as your own assistant text in a message whose only tool call is `true autoplan-published `, then enter the next phase in a later message. ### `publication_unflushed` The report shares the current call's message, or nothing journaled follows it yet. Publish it in its own message with the no-op call, then enter the next phase in a later message. A second occurrence in one invocation becomes [`publication_repeat`](#publication-repeat). ### `close_required` Finish the previous phase's close procedure and Read its close packet. ### `close_incomplete` Read every line of the current close packet. ### `close_stale` The close packet or its checkpoint no longer matches the current phase input. Prepare a fresh packet and read it. ### `close_edits` The plan changed after the close Read in a way the guard cannot reconstruct, or it fails its phase check. Repeat the close procedure. ### `init_required` No successful snapshot init for this invocation. Run the existing init step. ### `init_own` A new `/autoplan` turn needs its own init (it may answer `reused: true`). ### `init_failed` The most recent snapshot init call failed; re-run the same init command with the same three paths. An errored init is skipped, so a later successful init still binds. This denial applies when no init ever succeeded, or when an errored init follows a good one. ### `init_unbindable` Init ran through a shell variable, substitution, chaining, a pipe or a redirect. Re-run it as one Bash call with literal absolute paths. ### `phase_order` Enter Phase 1 (CEO) before a later phase. ### `hook_input` The hook could not read its input. Restore the installation (`./setup`). ### `installation` The hook failed unexpectedly. Restore the installation (`./setup`). ### `foreign_install` The phase file belongs to another gstack installation than the hook. Restore this installation (`./setup`). ## Transient denials ### `entry_pending` A phase-entry call from an earlier message has no result yet. Wait for it. ### `mutation_pending` An edit to the active plan after the close Read has no result yet. Wait for it. ## Removed codes ### `changing` Before the October 7, 2026 release, `changing` denied a call when Claude Code appended to the journal while the guard read it. The bounded reader reads the bytes present at open and ignores a trailing record still being written, so appends never fail a read; a changed prefix is [`rewritten`](#rewritten). Before the October 7, 2026 release, "Native parent evidence has not reached the journal yet" (no code) and "Current native phase-entry identity is required before entering a new phase" denied calls Claude Code had not yet journaled. The payload path replaced both. ## Claude Code settings `CLAUDE_CODE_FORK_SUBAGENT=false` is no longer needed (keeping it is harmless). With the fork-subagent gate on, reviewer dispatches may run in the background: the dispatch returns a launch receipt and the review arrives later as a completion notice. Wait for that notice before closing the phase. A background reviewer keeps the invocation armed, so a typed message while it runs does not end `/autoplan`. ### Permission cards after a background reviewer A skill's `allowed-tools` applies only to the turn that invoked it, and a background reviewer's completion notice starts a new turn. From then on Claude Code's normal permission rules apply, so Reads outside the session's working directory can show a card, and in Manual mode so do Bash commands that are not read-only. Skill hooks stay registered for the whole session but do not run inside subagents. `/autoplan` therefore keeps its own files readable without a card: - The restore point and every phase artifact (methodology, reviewer input, snapshots, close packet) live in the project's `.gstack/tmp/autoplan/`, which Step 1 adds to `.git/info/exclude`. Claude Code reads files in the working directory without a card, for `/autoplan` and for its reviewer subagents. A `git clean -x` deletes them, restore point included. - The publication hook approves `/autoplan`'s own Reads outside the working directory: this installation's `autoplan/sections/*.md`, and the immutable artifacts in `/.gstack/tmp/autoplan/` when the session started in a subdirectory of the repository. It approves nothing else, and never a call the guard denies. A session started in a repository subdirectory still shows a card when a reviewer reads its input: subagents run without the skill's hook. Start the session at the repository root, or answer the card with "Yes, allow reading from ... during this session". In Manual mode, `/autoplan`'s Bash commands after the first background reviewer also show cards; auto mode, the default starting mode on current Claude Code for supported models, sends them to its classifier instead. ## Supported journal roots The guard accepts exactly one root per journal file: - A user message with no parent (sessions without hooks). - An unbroken chain of message-less `SessionStart` hook attachments (`hook_success`, `hook_additional_context`, ...) that starts at a null parent and ends at the first user turn. Claude Code writes this when any `SessionStart` hook is installed, including hooks that plugins add. It also writes one after `/clear`. - A `compact_boundary` record whose logical parent is not in the file, but only when it is the file's first record. Claude Code writes this when you resume a compacted session with `--fork-session`. `--continue`, `--resume` and `/compact` append to the same journal, and the guard follows them through their existing parent links. The real journals these rules were checked against came from Claude Code 2.1.284 (`test/fixtures/claude-native-journal-roots-2.1.284.json`); the periodic `autoplan-journal-drift` canary re-checks the pinned Claude Code. ## The decision log Every guarded decision appends one line to `/analytics/autoplan-guard.jsonl` (normally `~/.gstack/analytics/autoplan-guard.jsonl`): `schema`, time, `decision` (`allow` or `deny`), `disposition`, `code`, `path` (`journal`, `payload` or `none`) and the Claude Code version. It never holds content; an `agent_key` denial adds the key names, and an unrecognized shape adds record type names. The file keeps the newest 1,000 lines once it passes 256 KiB. A failure to write it never changes a decision. ## Windows paths Claude Code and Git Bash can spell one path as `C:\x`, `c:\x`, `C:/x` or `/c/x`. The guard treats these as the same path. It does not resolve `.` or `..`, so a path containing them is still rejected. ## Symlinked plan directories (macOS /tmp, /var) The guard compares plan paths canonically, not by argv spelling. On macOS `/tmp` and `/var` are symlinks to `/private/tmp` and `/private/var`, so a plan under `/tmp` or `/var/folders` is expected to appear as `/private/...` in the init JSON. That is by design: `init` reports canonical paths (the deepest existing ancestor is resolved with `realpath` and the missing segments are re-joined, `canonicalDestination` in `bin/gstack-autoplan-snapshot.ts`), and the guard canonicalizes the init command's `` and `` the same way. Writes and Edits to the active plan are matched by canonical path too, so editing the plan through an alias spelling is recognized. Symlinked plan directories are allowed; only a result that differs from the canonical form of its own arguments is denied (`init_mismatch`).