Skip to content

feat(claude-mod): turn the Claude Code mod on by default - #1686

Merged
backnotprop merged 4 commits into
mainfrom
feat/claude-mod-default-on
Oct 4, 2026
Merged

backnotprop merged 4 commits into
mainfrom
feat/claude-mod-default-on

Conversation

@backnotprop

Copy link
Copy Markdown
Owner

Decision

The owner's call: "we just need to fully embrace it." The Claude Code mod is now on by default for every Claude Code that runs hooks modules (2.1.287+, interactive CLI). The mod still stands down by itself in -p / SDK sessions and where /bin/sh is missing (Windows).

What changes for users

  • Interactive Claude Code 2.1.287+ users get non-blocking plan review, /plannotator-review, /plannotator-annotate, /plannotator-last, the plannotator tool and Ask this session without setting anything. Decisions arrive later as plugin turns.
  • Opt-out: PLANNOTATOR_CLAUDE_MOD=0 (also false / off / disabled) or { "claudeCodeMod": false } in ~/.plannotator/config.json. The env var wins. 1 / true / on still force it on. An empty or unrecognized value now counts as unset, which means on.
  • With the knob off, the module is inert exactly as before: the classic PermissionRequest hook and the skills run.

resolveClaudeCodeMod (packages/shared/config.ts) and its mirror apps/hook/hooks/mod/enabled.ts both default to true. enabled.test.ts still compares the two across every env × config combination.

Version skew (verified with real binaries)

The plugin installs from main and the plannotator binary updates separately. With default-on, users will run the new mod against an old CLI. I ran the real mod controller (worktree source) over a Host backed by real processes and the real filesystem. In each run the released binary was first on PATH, with a temp data dir and a temp git repo. Decisions were made by POSTing to the launched server, as the browser does. Throwaway harness: /tmp/skew/harness.ts + run.sh (not committed).

Flow 0.27.25 (latest release) 0.27.10 (before #1444) this branch's CLI
ExitPlanMode claude-mod-plan: Unknown command, exit 1 in ~0.3 s. Falls back to the classic blocking review (the plugin's PermissionRequest hook on the old binary; checked separately: it opens, and a deny returns the hook JSON). Before this PR: an "could not open Plan vN (exit 1): Unknown command…" log line and a failed launch with a leftover plan copy on every ExitPlanMode. Now: one explanatory line, no further probes, launch dir removed. Reads the unknown subcommand as hook mode: No plan content in hook event, exit 1. Same fallback. Non-blocking: deny + review opens; approval submitted; the next ExitPlanMode passes and PermissionRequest allows with the approved text
/plannotator-review Works; decision delivered from stdout (legacyResult). Before this PR an LGTM was delivered as "Changes requested" with the "no changes requested" prompt, and approve-with-notes was also labelled "Changes requested". Now an LGTM only logs and with-notes is "Approved with notes". Close only logs. Works Works (result record)
/plannotator-annotate Works: feedback delivered, Done/Close only log; missing file shows the CLI error Works Works
/plannotator-last Works: PLANNOTATOR_HOST_MESSAGES_FILE is ignored and the newest message opens from stdin (no picker). The command line still says "2 messages, newest first" (cosmetic). Works Picker with both messages
plannotator tool (annotate --gate, review --base) Works: the gated approval is delivered as a turn, review feedback is delivered Works Works
Ask this session /api/ai/bridge/poll → 404; the bridge stops and Ask AI shows only providers same Bridge live

So the earlier outline's claim ("plan review fails because the old version is missing a command") was half right. The command is missing, but the mod already fell back to the classic flow. What users actually got was a scary error line on every plan, which this PR replaces with one line telling them to update the CLI. Plan review with an old CLI is blocking, not broken. CLIs older than 0.19.24 write no ready file, so commands wait their full 45 s / 15 s and say "Starting…"; decisions still arrive at exit.

Tests

  • bun test apps/hook packages/shared (temp PLANNOTATOR_DATA_DIR): 1774 pass, 0 fail
  • bun test apps/hook/hooks/mod: 49 pass. New tests:
    • default-on and opt-out cases in enabled.test.ts
    • CLI without claude-mod-plan (both observed stderr shapes) in controller.test.ts
    • legacyResult with the observed old outputs, plus the copied default prompts pinned to packages/shared/prompts.ts, in delivery.test.ts
  • scripts/test-claude-code-mod.sh (engine harness, Claude Code 2.1.289): 11 pass. Tests now run with no knob set (default on). The two knob-off tests use PLANNOTATOR_CLAUDE_MOD=0 and claudeCodeMod: false. New: an old CLI's ExitPlanMode reaches the classic flow twice with one launch and one log line.
  • tsc -p apps/hook/hooks/mod/tsconfig.json: clean
  • claude plugin validate apps/hook: passed

Still risky with default-on

  • No edit guard. While a plan review is open, the session is not blocked. Plan mode stays on and Claude was told "NOT approved, do not implement". But a user who leaves plan mode (Shift+Tab) and keeps talking can get edits before approval, which the classic blocking review made impossible. This only happens if the user leaves plan mode themselves, so it is not built here. The spec's guard (deny Edit/Write while a plan is in review) needs mode tracking and a UX for "I meant to do other work", so it is not a small, clearly safe change.
  • A user-customized prompts.review.approved on an old CLI still arrives as feedback ("Changes requested"): stdout cannot tell it apart.

@backnotprop

Copy link
Copy Markdown
Owner Author

Review follow-up in 257cd21: (1) the timer skips a launch while a hook is still waiting on it (starting), so an old CLI's refusal is reported once, by the hook; (2) a refusal that lands after the hook's 15 s wait is caught by the timer: classic fallback, the update line, and a turn asking Claude to call ExitPlanMode again; (3) the classic flag stays per instance (a resume probes once more and picks up an updated CLI); AGENTS.md now says so. Two new controller tests, both shown failing without the fix. bun test apps/hook packages/shared: 1776 pass; mod tsc clean; engine harness 11 pass; plugin validate passed.

A CLI without claude-mod-plan (0.27.11+: 'Unknown command'; older:
'No plan content in hook event') now sends the session's plans to the
classic review with one explanatory log line, instead of an error line
and a fresh failed launch (with a leftover plan copy) on every
ExitPlanMode. An old CLI's default review approval prompt is read as an
LGTM and its approved-with-notes prompt as 'Approved with notes', not
'Changes requested'.
PLANNOTATOR_CLAUDE_MOD=0|false|off|disabled or { "claudeCodeMod": false }
turns it off; unset, empty or unrecognized now means on. The -p/SDK and
no-/bin/sh stand-downs are unchanged.
…hen it comes late

The timer leaves a launch alone while a hook is still waiting for it to
come up (no second, wrong 'stopped' line). An old CLI that refuses
claude-mod-plan after the hook stopped waiting is caught by the timer:
classic fallback plus a turn asking Claude to call ExitPlanMode again.
The classic flag is per instance on purpose; AGENTS.md says so.
The take-over test's knob-off case used the removed 'enabled: false'
option, so it ran with the mod on; it now sets PLANNOTATOR_CLAUDE_MOD=0.
AGENTS.md: the default-on paragraph, env row and version-skew list name
the plannotator tool and the Bash take-over.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant