Skip to content

Commit 5990aa3

Browse files
phernandezclaude
andcommitted
fix(plugins): hooks fall back to uvx/uv when no CLI binary is on PATH
Codex flagged (review 4397232451) that the hooks locate the CLI via `command -v basic-memory || command -v bm`, but our own README recommends connecting the MCP server as `claude mcp add basic-memory -- uvx basic-memory mcp` — an ephemeral uv run that leaves NO binary on PATH. In that (recommended) setup both hooks exited before doing anything, so the bridge silently did nothing even though the MCP server worked. Both hooks now resolve the CLI invocation as: prefer a `basic-memory`/`bm` binary (fast), else fall back to `uvx basic-memory`, else `uv tool run basic-memory`, else silent no-op. The launcher may be multi-token, so the embedded Python splits it with shlex and prepends it to each command list. The uv cache is already warm from running the MCP server, so the fallback is cheap. Verified: with no binary on PATH but uvx present, SessionStart now produces a brief (previously a silent no-op). Updated the README requirement to note uvx-only works. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 89d20f6 commit 5990aa3

3 files changed

Lines changed: 46 additions & 14 deletions

File tree

plugins/claude-code/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ Plugin skills are namespaced under the plugin name:
4646
## Requirements
4747

4848
- [Basic Memory](https://github.com/basicmachines-co/basic-memory) `>= 0.19.0`
49-
installed and configured as an MCP server (`uv tool install basic-memory`).
49+
connected as an MCP server. `uv tool install basic-memory` is recommended (it puts
50+
a `basic-memory` binary on PATH, which the hooks call directly). A `uvx
51+
basic-memory mcp`-only setup also works — the hooks fall back to `uvx`/`uv` when no
52+
binary is on PATH.
5053
- Claude Code.
5154

5255
## Installation

plugins/claude-code/hooks/pre-compact.sh

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,33 @@ set -u
1919

2020
input="$(cat 2>/dev/null || true)"
2121

22-
BM="$(command -v basic-memory || command -v bm || true)"
23-
[ -z "$BM" ] && exit 0
22+
# Resolve how to invoke the Basic Memory CLI — prefer a binary on PATH, fall back to
23+
# uvx / uv so checkpointing still works when BM was connected only as an ephemeral
24+
# `uvx basic-memory mcp` server (no persistent CLI). Silent no-op if none available.
25+
if command -v basic-memory >/dev/null 2>&1; then
26+
BM="basic-memory"
27+
elif command -v bm >/dev/null 2>&1; then
28+
BM="bm"
29+
elif command -v uvx >/dev/null 2>&1; then
30+
BM="uvx basic-memory"
31+
elif command -v uv >/dev/null 2>&1; then
32+
BM="uv tool run basic-memory"
33+
else
34+
exit 0
35+
fi
2436

2537
BM_HOOK_INPUT="$input" BM_BIN="$BM" python3 <<'PY' 2>/dev/null || exit 0
2638
import json
2739
import os
2840
import re
41+
import shlex
2942
import subprocess
3043
import sys
3144
from datetime import datetime
3245
33-
bm = os.environ.get("BM_BIN", "basic-memory")
46+
# May be a single binary ("basic-memory") or a multi-token launcher
47+
# ("uvx basic-memory"); split so it prepends cleanly onto the write command.
48+
bm_cmd = shlex.split(os.environ.get("BM_BIN") or "basic-memory")
3449
3550
# A project ref can be a workspace-qualified name (route via --project) or an
3651
# external_id UUID (route via --project-id) — names collide across workspaces, so
@@ -200,7 +215,7 @@ project_flag = "--project-id" if UUID_RE.match(primary_project) else "--project"
200215
try:
201216
subprocess.run(
202217
[
203-
bm, "tool", "write-note",
218+
*bm_cmd, "tool", "write-note",
204219
"--title", title,
205220
"--folder", capture_folder,
206221
project_flag, primary_project,

plugins/claude-code/hooks/session-start.sh

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,24 @@ set -u
2222
# stdin can only be consumed once; capture it before anything else touches it.
2323
input="$(cat 2>/dev/null || true)"
2424

25-
# --- Locate the Basic Memory CLI ---
26-
# Trigger: bm not installed / not on PATH.
27-
# Why: the plugin is useful on its own merits to BM users; for everyone else it
28-
# must be invisible. No binary → no brief, no error.
29-
# Outcome: silent no-op.
30-
BM="$(command -v basic-memory || command -v bm || true)"
31-
[ -z "$BM" ] && exit 0
25+
# --- Resolve how to invoke the Basic Memory CLI ---
26+
# Prefer a binary on PATH (fast — no per-call env resolution). Fall back to uvx / uv
27+
# so the hook still works when Basic Memory was connected only as an ephemeral
28+
# `uvx basic-memory mcp` server (the MCP setup our README recommends) with no
29+
# persistent CLI installed — the uv cache is already warm from running the server.
30+
# Trigger: none of basic-memory / bm / uvx / uv on PATH → BM isn't usable here.
31+
# Outcome: silent no-op (the plugin must be invisible to non-BM users).
32+
if command -v basic-memory >/dev/null 2>&1; then
33+
BM="basic-memory"
34+
elif command -v bm >/dev/null 2>&1; then
35+
BM="bm"
36+
elif command -v uvx >/dev/null 2>&1; then
37+
BM="uvx basic-memory"
38+
elif command -v uv >/dev/null 2>&1; then
39+
BM="uv tool run basic-memory"
40+
else
41+
exit 0
42+
fi
3243

3344
# Everything else runs in one Python pass: parse config, run the queries, format
3445
# the brief. Python is a guaranteed dependency (basic-memory requires it) and
@@ -38,11 +49,14 @@ BM_HOOK_INPUT="$input" BM_BIN="$BM" python3 <<'PY' 2>/dev/null || exit 0
3849
import json
3950
import os
4051
import re
52+
import shlex
4153
import subprocess
4254
import sys
4355
from concurrent.futures import ThreadPoolExecutor
4456
45-
bm = os.environ.get("BM_BIN", "basic-memory")
57+
# May be a single binary ("basic-memory") or a multi-token launcher
58+
# ("uvx basic-memory"); split so it prepends cleanly onto each command list.
59+
bm_cmd = shlex.split(os.environ.get("BM_BIN") or "basic-memory")
4660
4761
# Cloud project refs come in two unambiguous forms (names collide across
4862
# workspaces, so a bare name won't route): a workspace-qualified name like
@@ -119,7 +133,7 @@ shared_refs = shared_refs[:MAX_SHARED]
119133
# project_ref=None routes to the user's default project (zero-config usefulness).
120134
# A UUID ref routes via --project-id; a qualified name via --project.
121135
def search(filters, project_ref=None, timeout=10):
122-
cmd = [bm, "tool", "search-notes", *filters, "--page-size", "5"]
136+
cmd = [*bm_cmd, "tool", "search-notes", *filters, "--page-size", "5"]
123137
if project_ref:
124138
flag = "--project-id" if UUID_RE.match(project_ref) else "--project"
125139
cmd += [flag, project_ref]

0 commit comments

Comments
 (0)