Skip to content

fix(hooks): only point widget hooks at ccstatusline's own command - #674

Open
eric-engberg wants to merge 1 commit into
sirmalloc:mainfrom
eric-engberg:fix/skills-hooks-foreign-command
Open

eric-engberg wants to merge 1 commit into
sirmalloc:mainfrom
eric-engberg:fix/skills-hooks-foreign-command

Conversation

@eric-engberg

Copy link
Copy Markdown
Contributor

What

Saving settings no longer adds Claude Code hooks that run another tool's status line script. Widget hooks (the Skills widget's PreToolUse and UserPromptSubmit hooks) are only written when Claude's statusLine.command runs ccstatusline. Otherwise the sync only removes ccstatusline-managed hooks.

Why

Every ctrl+s and Save & Exit syncs widget hooks into Claude's settings.json, and each hook runs <statusLine.command> --hook. The sync never checked what that command was. With ccstatusline configured but another status line installed in Claude Code, saving a line that has the Skills widget turns this:

{ "statusLine": { "type": "command", "command": "bash ~/.claude/statusline.sh" } }

into this:

"hooks": {
  "PreToolUse": [{ "_tag": "ccstatusline-managed", "matcher": "Skill", "hooks": [{ "type": "command", "command": "bash ~/.claude/statusline.sh --hook" }] }],
  "UserPromptSubmit": [{ "_tag": "ccstatusline-managed", "hooks": [{ "type": "command", "command": "bash ~/.claude/statusline.sh --hook" }] }]
}

That script then runs on every prompt and every Skill call with an argument it doesn't know. Claude Code adds UserPromptSubmit output to the conversation, so whatever the script prints (usually a status line) reaches the model on each turn. #623 describes the same injection for a wrapped ccstatusline command; here it happens with a command that isn't ccstatusline at all.

How

syncWidgetHooks now adds hooks only when the status line command contains ccstatusline. That's the same test the existing cleanup uses to recognize ccstatusline hook commands (CCSTATUSLINE_HOOK_PATTERN), so every hook written can still be recognized as ours later.

The check is deliberately looser than isKnownCommand, which only accepts the exact commands the installer writes. These commands keep their hooks, as before:

  • npx/bunx with any version or none, such as bunx -y ccstatusline, which the README measures as faster per repaint than @latest;
  • a global or absolute-path binary (/Users/me/.bun/bin/ccstatusline);
  • a local ccstatusline.ts checkout;
  • a shell-wrapped command such as bash -c 'head -1 | ccstatusline'.

The last one is the case in #623. It still counts as ccstatusline's own, so this works alongside #641, which makes ccstatusline handle the hook payload when the wrapper drops --hook.

For any other command (or none), the sync takes the existing "no status line" path: it strips ccstatusline-managed and legacy ccstatusline hooks and writes nothing new. That also removes hooks an earlier save pointed at another tool's script. Hooks belonging to other tools are left alone, as before.

One side effect: a personal wrapper script that calls ccstatusline but doesn't have ccstatusline in its own command string no longer gets hooks. It can't be told apart from another tool's script, and running an unknown script on every prompt is the bug being fixed.

Demo

Nothing visible changes in the TUI or the status line; only the hooks written to Claude's settings.json differ (shown above).

Testing

  • New tests in hooks.test.ts: with bash ~/.claude/statusline.sh as the status line and the Skills widget, sync leaves only the non-ccstatusline hook and drops a managed entry pointing at the script. This test fails on main. A shell-wrapped bash -c 'head -1 | ccstatusline' command still gets both hooks; this one passes on main too and guards the wrapped case.

  • bun test: 2784 pass, 0 fail. bun run lint passes.

  • Under Node 26.10.0 (Vitest): hooks.test.ts 7 pass (5 existing, 2 new). The full suite under Node gives 2609 of 2762 passing. The 153 failures are in the same 17 files that fail on main, all from spying on Node built-ins.

  • Built CLI under Bun 1.4.2 and Node 26.10.0: piped renders in plain and Powerline modes agree across runtimes and match main. The TUI opens and exits cleanly under both.

  • End to end, with the built TUI under Bun and Node, a scratch HOME and CLAUDE_CONFIG_DIR, and a Skills widget on the line, pressing ctrl+s:

    • with bash ~/.claude/statusline.sh as the status line, main writes both hooks running bash ~/.claude/statusline.sh --hook, and this branch writes none;
    • with npx -y ccstatusline@latest or bash -c 'head -1 | ccstatusline', this branch writes both hooks with that command plus --hook.

    In every run the other settings (model) stayed intact.

Saving settings with a hook-enabled widget (Skills) syncs Claude Code
hooks that run `<statusLine.command> --hook`. That happened whatever the
status line command was. With ccstatusline configured but another tool
installed as the status line (for example `bash ~/.claude/statusline.sh`),
every ctrl+s or Save & Exit added PreToolUse(Skill) and UserPromptSubmit
hooks running that script with `--hook`. The script then ran on every
prompt and Skill call, and Claude Code adds UserPromptSubmit output to
the conversation, so whatever it printed reached the model.

Hooks are now added only when the status line command contains
`ccstatusline`, the same test the legacy-hook cleanup already uses to
recognize ccstatusline hook commands. npx/bunx with any version, a global
or absolute-path binary, a local ccstatusline.ts checkout and
shell-wrapped commands keep their hooks. For any other command the sync
only removes ccstatusline-managed entries, which also cleans up hooks an
earlier save pointed at another tool's script.
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