Skip to content

--doctor reports the PostToolUse hook as missing when bully is installed as a Claude Code plugin #19

Description

@mattes3

Summary

bully --doctor always prints [FAIL] no PostToolUse hook invoking hook.sh found in .claude/settings.json — and exits 1 — when bully is installed as a Claude Code plugin, even though the hook is wired correctly and firing. The check only scans .claude/settings.json, which is the manual-install location; it has no awareness of the hooks/hooks.json the plugin ships and registers itself.

The same run of --doctor reports the evaluator agent and all four skills as (plugin install), so the tool already knows it is looking at a plugin. The hook check is the one probe that hasn't learned about that install mode.

Version

  • bully 0.8.6 (latest release), installed via claude plugin install bully@bully-marketplace
  • Claude Code, macOS 15 (Darwin 25.5.0), Python 3.14

Reproduction

  1. Install bully as a Claude Code plugin (no manual .claude/settings.json hook entry).
  2. Confirm the hook actually works — SessionStart prints bully active. N rules configured, and edits are linted.
  3. Run bully --config .bully.yml --doctor.

Actual

[OK] Python 3.14
[OK] config present at /path/to/repo/.bully.yml
[OK] config parses (16 rules)
[OK] config trusted on this machine (c22e12327596...)
[OK] ast-grep on PATH (8 engine:ast rule(s))
[FAIL] no PostToolUse hook invoking hook.sh found in .claude/settings.json
[OK] evaluator agent at /Users/<me>/.claude/plugins/cache/bully-marketplace/bully/0.8.6/agents/bully-evaluator.md (plugin install)
[OK] skill bully present at /Users/<me>/.claude/plugins/cache/bully-marketplace/bully/0.8.6/skills/bully/SKILL.md (plugin install)
[OK] skill bully-init present at ... (plugin install)
[OK] skill bully-author present at ... (plugin install)
[OK] skill bully-review present at ... (plugin install)

echo $?1.

Expected

Something like [OK] PostToolUse hook wired in <plugin>/hooks/hooks.json (plugin install), and exit 0.

Cause

src/bully/cli/doctor.py:97-123 looks for a PostToolUse entry whose command string contains hook.sh, in exactly two files:

for settings in (
    Path.cwd() / ".claude" / "settings.json",
    Path.home() / ".claude" / "settings.json",
):

A plugin install writes neither. The wiring lives in the plugin's own hooks/hooks.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/hook.sh\"" }
        ]
      }
    ]
  }
}

Line 123 sets ok = False, so this also makes --doctor unusable as a health gate in CI for plugin installs — the exit code can never be 0.

Suggested fix

Fall back to the plugin cache when neither settings file has the hook: glob ~/.claude/plugins/cache/*/bully/*/hooks/hooks.json, parse it, and apply the existing PostToolUsehooks[].command contains hook.sh test. plugin_cache_candidates() at doctor.py:31 already does this glob for skills/agents, but its path shape is name-based (<name>/SKILL.md, <name>.md), so hooks need a small variant rather than a reuse.

Worth also checking .claude/settings.local.json while in there — it's a third place a user can wire the hook manually today, and it's equally invisible to the current check.

Why it matters

The message reads as "your linting isn't running", which is the opposite of the truth. The obvious user reaction is to add a redundant PostToolUse entry to .claude/settings.json to make it go green — which registers hook.sh a second time and double-runs every check on every edit.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions