Claude Code plugin¶
downbeat ships as a native Claude Code plugin
(.claude-plugin/plugin.json + hooks/hooks.json, at the repo root) — an
optional, Claude-Code-only fast path alongside downbeat init's
existing hand-merge into settings.json. It doesn't replace init: downbeat
is a general local message bus, not a Claude-Code-only tool, so init's
settings.json hand-merge remains the baseline path that works everywhere.
Updating: use /downbeat:update, not claude plugin update alone¶
claude plugin update does not update the downbeat CLI. It moves the
plugin only. This surprises everyone exactly once, so it's worth being blunt
about why.
A Claude Code plugin cannot ship a terminal command. Plugin bin/ puts
executables on the Bash tool's PATH — reachable when Claude shells out
inside a session, invisible to a terminal you open yourself. There is also no
install-time hook: the hook event list has no PluginInstall/PluginUpdate,
so nothing can run at update time to go fetch the CLI. Those are properties of
the plugin system, not gaps in this project.
So downbeat is two artifacts with two update paths, and the plugin can only move one of them:
| artifact | what it is | updated by |
|---|---|---|
| plugin | hooks, slash commands, skills | claude plugin update downbeat@downbeat |
| CLI / TUI | the downbeat command |
uv tool upgrade downbeat |
Run /downbeat:update and forget the table — it does both and reports what
moved.
Two things guard the gap for when you forget:
- The plugin's
SessionStarthook compares your CLI's version against the plugin's and speaks up if they've drifted. It stays quiet when they agree, and quiet on editable installs — there the version is stamped at install time while the code is read live from a working tree, so a mismatch is expected and means nothing. downbeat --versionreports where the code came from, not just a number:
downbeat 0.9.2
downbeat 0.9.2 (editable → /path/to/checkout; this number is from install
time, the code is whatever is checked out there now)
That second form matters. A bug once got filed against the version
--version printed while the code actually running was several releases
ahead; the mismatch sent the investigation the wrong way twice.
A running TUI holds the code it loaded at launch. After any update, quit
and relaunch downbeat tui — otherwise you are still on the old one, and no
amount of updating will show it.
Install¶
Two commands, nothing to fill in — copy, paste, run:
claude plugin marketplace add FreddieMcHeart/downbeat
claude plugin install downbeat@downbeat
No local clone needed: the first command points Claude Code at this GitHub
repo (it shallow-clones the marketplace manifest under
~/.claude/plugins/marketplaces/), the second installs the plugin from that
source. Then start a new Claude Code session (or restart your current one)
so the hooks actually load.
Verify it worked:
claude plugin list --json | grep -A2 '"id": "downbeat@downbeat"'
Should show "enabled": true.
If you're working from a local clone instead (e.g. testing an unmerged branch, or you keep the repo checked out anyway), point at the path instead of the GitHub shorthand:
claude plugin marketplace add /path/to/your/downbeat/checkout
claude plugin install downbeat@downbeat
(the name@marketplace form on the install command disambiguates if you
ever have another plugin also named downbeat registered from a different
marketplace).
How coexistence works¶
downbeat init checks whether the downbeat plugin is installed and
enabled (claude plugin list --json) before hand-merging hooks into
settings.json:
- Plugin enabled, no prior
initrun: hand-merge is skipped entirely — the plugin's ownhooks/hooks.jsonis natively loaded by Claude Code, so there's nothing forinitto add. - Plugin enabled,
initwas already run before you installed the plugin:initprints an explicit WARNING about double-firing (both the old hand-merged entries insettings.jsonand the plugin's own hooks would fire on every event) instead of silently doing nothing. - Plugin not detected (not installed, or the
claudeCLI itself isn't onPATH):initfalls back to today's hand-merge, unchanged.
The check fails open — any error running claude plugin list --json
(missing binary, timeout, bad JSON) is treated as "plugin not detected", so
a broken or absent Claude Code CLI never blocks init's fallback path.
If you get the double-fire warning¶
Run:
downbeat init --migrate-to-plugin
This removes exactly the hand-merged entries the original init run wrote —
matched by exact command string against hooks_manifest.json, so any other
hook sharing the same event/matcher (e.g. cost-discipline.py) is left
untouched — and backs up settings.json first. It refuses to run unless the
plugin is actually installed and enabled, so it can never leave you with no
working relay hooks.
If a legacy entry doesn't get removed (e.g. its command string no longer
byte-matches today's derivation — $HOME changed, a symlink resolved
differently), the command tells you and points at downbeat uninstall as
the substring-based fallback.
--migrate-to-plugin is a standalone mode of init — it does not also
re-run the rest of init (skill/shim/hooks/commands installation), since
those are plugin-irrelevant once the plugin owns hook registration.
downbeat init --force does not do this for you — it only re-verifies
the hand-merge path, which is exactly what you're trying to stop using.