Skip to content

feat(claude-mod): agent-run plannotator commands open through the mod, so Ask AI reaches the session - #1688

Merged
backnotprop merged 1 commit into
mainfrom
feat/take-over-agent-cli-runs
Oct 4, 2026
Merged

backnotprop merged 1 commit into
mainfrom
feat/take-over-agent-cli-runs

Conversation

@backnotprop

@backnotprop backnotprop commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

The bug

Owner report: annotating HTML showed the provider options in Ask AI instead of only "Ask this session". This is not specific to HTML. Evidence from one Claude Code session with the mod on:

How it was opened Session bridge Ask AI
plannotator annotate .../INDEX.html --gate --json (Claude ran it in Bash) none every SDK provider
plannotator annotate .../REVIEW-3.md --gate --json (Claude ran it in Bash) none every SDK provider
plannotator annotate-last --stdin (started by the mod) bridge env present works

Cause: Claude ran the CLI itself through Bash instead of using the plannotator tool. That run blocks the session and starts a server without a bridge token. The tool is deferred behind tool search, and the CLI is documented, so models still reach for Bash. #1687 improved the skill wording; this PR makes the Bash path behave the same as the tool.

Design

  • One host-neutral parser, in the shared contract. plannotatorCommandToToolInput(command) lives in the CONTRACT section of packages/shared/plannotator-tool.ts, and the mod keeps its byte-for-byte copy in hooks/mod/tool.ts (enforced by tool.test.ts). It turns a shell command into the plannotator tool's input, or returns null for a command that should run unchanged.
  • Quoting. simpleShellCommandWords follows the same quoting rules as splitShellWords and expands nothing. It returns null for anything a shell would interpret.
  • The mod's take-over (hooks/mod/take-over.ts): the existing tool.call hook, on the main loop's Bash only, sends a matching call through PlannotatorMod.runTool, the same launch the plannotator tool uses. That launch is detached, carries the result file and the bridge token, and delivers the decision later as a plugin turn.
    • The command never runs. Claude gets back a normal Bash result, { stdout: <the tool's opened text>, stderr: "", interrupted: false }; a startup error comes back as a deny.
    • A gated take-over delivers its bare approval as a turn, exactly like a gated tool call.
    • register.ts is still the only file with $ calls, and controller.ts is unchanged.
  • Main loop only. A subagent may run in its own cwd or worktree, while the mod launches in the session's cwd. Its review or annotate notes.md would therefore open the wrong diff or file, so a subagent's command always runs for real.

Taken over vs. passed through

Taken over only when all of these hold:

  • one simple command;
  • the program is exactly plannotator;
  • the subcommand and arguments are one of:
    • annotate <one target> [--gate] [--markdown]
    • review [one target] [--base <ref>]
    • annotate-last or last, with no arguments
  • --json is accepted and dropped;
  • the result passes parsePlannotatorToolInput.

Passed through unchanged (the real CLI runs):

  • a path to the binary (./plannotator, /tmp/dev/plannotator): that is a dev build;
  • every other flag: --require-approval, --result-file, --hook, --tailscale, --static, --app, --no-jina, --render-html, --diff-type, --local, --patch-file, --stdin, --help, ...
  • repeated flags, several targets, other subcommands;
  • env-var prefixes, npx, env;
  • any shell syntax: ; & | < > ( ), line breaks, $, backticks, unquoted globs or braces, # comments, ~user, an unterminated quote. This covers cd x && plannotator ....

So scripted strict gates keep the real CLI and its exit codes.

Not in this PR

Pi and OpenCode 2 are held. Their wiring is preserved on branch feat/take-over-agent-cli-runs-pi-opencode (no PR):

  • OpenCode 2: the Promise-plugin adapter re-wraps the shell tool's execute, which turns any shell Tool.Error into an uncaught defect.
  • Pi: there is no plannotator tool, and the only way to answer a call is a block whose result is flagged as an error, which invites a retry through the blocking CLI.

On both hosts an agent-run plannotator command still runs the CLI.

Tests

  • packages/shared/plannotator-tool-command.test.ts: the parser's take-over and pass-through rules, including a dev build run by path, shell syntax, and quoting.
  • apps/hook/hooks/mod/take-over.test.ts (in-memory Host):
    • a Bash plannotator annotate INDEX.html --gate --json launches exactly what the tool launches, with the bridge token and the result file, and answers with the tool's text;
    • a gated approval is delivered;
    • a startup error is a deny;
    • --require-approval, --result-file, a piped command and cd x && ... launch nothing;
    • every subagent command runs as written;
    • a dev build run by path runs for real.
  • apps/hook/tests/register.test.ts (engine harness): the take-over through the real engine; pass-through reaching the core Bash, covering a subagent and ./plannotator; and the knob off.

Runs:

  • bun test apps/hook packages/shared with a temp PLANNOTATOR_DATA_DIR: 1784 pass, 0 fail.
  • scripts/test-claude-code-mod.sh: 13 pass.
  • bun run typecheck (which includes the mod's tsconfig) and claude plugin validate apps/hook: clean.

…, so Ask AI reaches the session

Claude sometimes runs `plannotator annotate x.html --gate --json` in Bash
instead of calling the plannotator tool; that blocked the session and started
a server with no bridge token, so Ask AI offered separate SDK agents. One
host-neutral parser, plannotatorCommandToToolInput (shared tool contract,
copied into the mod), decides what is taken over; the mod's tool.call hook
answers a matching main-loop Bash call through the same launch as the tool.
Strict gates, other flags, several targets, compound commands, a dev build
run by path, and every subagent's command run unchanged.
@backnotprop
backnotprop force-pushed the feat/take-over-agent-cli-runs branch from 2478fc4 to 16c70c7 Compare October 4, 2026 21:25
@backnotprop backnotprop changed the title feat: agent-run plannotator commands open through the host, so Ask AI always reaches the session feat(claude-mod): agent-run plannotator commands open through the mod, so Ask AI reaches the session Oct 4, 2026
@backnotprop
backnotprop merged commit 781a6ce into main Oct 4, 2026
28 checks passed
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