Skip to content

feat(core): add bm hook producer front door for harness capture#1070

Merged
phernandez merged 48 commits into
mainfrom
feat/997-bm-hook-front-door
Jul 20, 2026
Merged

feat(core): add bm hook producer front door for harness capture#1070
phernandez merged 48 commits into
mainfrom
feat/997-bm-hook-front-door

Conversation

@phernandez

Copy link
Copy Markdown
Member

Summary

Implements the harness capture producer front door (SPEC-55 revision 2026-07-15, the plan posted on #997). Coding-agent lifecycle hooks are captured as normalized, redacted producer envelopes in a local WAL and projected into knowledge-graph artifacts out of band — with the plugins reduced to configuration and all logic living in the basic-memory package where it is tested, typed, and released.

Supersedes #998 and #1064: the envelope design from those PRs carries forward into basic_memory/hooks/ with commit credit to @sourrrish. The prior attempts embedded Python in shell heredocs and vendored a shared module that never survived packaging; this replaces both with a CLI front door.

Architecture

  • Plugins ship zero logic. Claude Code and Codex hooks are one-line shims that exec bm hook <event> --harness <name> with stdin passthrough. bm is resolved installed-first (BM_BINbasic-memory/bm on PATH), falling back to uvx "basic-memory>=<floor>" so no local install is required; uv is the documented per-platform prerequisite and the floor is bumped by the release version updater.
  • bm hook CLI (basic_memory/hooks/ + cli/commands/hook.py): per-harness stdin adapters → envelope → Stage-1 redaction floor → atomic WAL inbox. Verbs: session-start, pre-compact, flush, status, install, remove. Execution is fail-open (a hook never disrupts a session); the captureEvents gate is fail-closed (must be boolean true).
  • WAL inbox at <bm-home>/inbox/<uuid7>.json (atomic tmp+rename, one file per envelope). UUIDv7 ids sort chronologically, so the projector processes in capture order with no mtime dependence, and ids double as the caused_by target. Lives in BM home by requirement — plugin dirs are ephemeral and deleted on uninstall.
  • Deterministic projector (bm hook flush): sweeps the whole inbox, groups by session, dedups on idempotency key (envelopes are hints, never double-written), and projects SessionNote + ToolLedger artifacts carrying created_by + caused_by_event provenance. No LLM — the SPEC-54 daemon worker supersedes it later without reworking the inbox contract.
  • Two-stage redaction: the Stage-1 deterministic floor ships here (detect-secrets + recursive deny-key/deny-path/entropy/truncation). The Stage-2 model PII scrub at the promotion gate is phase 2.

Review

Built and reviewed via a multi-agent workflow: five review lenses (correctness, security/privacy, portability, platform conformance, style/tests) over the branch diff, each finding adversarially verified. Five confirmed findings were fixed with regression tests (final commit):

  • projector rebuilt session artifacts from only still-pending envelopes, erasing events projected on an earlier sweep — now merges the processed history.
  • 8-char session-id title truncation let distinct sessions collide on one permalink — now uses the full id.
  • concurrent mark_processed crashed the sweep on a double-retire — now tolerated.
  • retention could delete a non-v7 UUID file on a fabricated timestamp — uuid7_unix_ms now fails fast.
  • pre-compact checkpoint transcript excerpts bypassed the redaction floor — now redacted before entering the note (the Capture Claude/Codex harness events as Basic Memory producer envelopes #997 "redact before writing" constraint).

Test plan

  • uv run pytest tests/hooks tests/cli/test_hook_command.py — 146 passed (unit coverage of uuid7, envelope, redaction floor + redact_text, inbox atomicity/ordering/retention/concurrency, adapters against recorded fixtures, projector dedup/replay-safety/incremental-flush/mapping-gate, and the CLI verbs incl. captureEvents fail-closed and checkpoint redaction).
  • uv run ruff check + ruff format — clean.
  • uv run ty check src/basic_memory/hooks src/basic_memory/cli/commands/hook.py — clean.
  • just package-check — passes (Claude plugin validate --strict, Codex layout, Hermes, OpenClaw, skills).

Fixes #997
Closes #998

🤖 Generated with Claude Code

phernandez and others added 9 commits July 15, 2026 13:41
…t door

Port the SPEC-55 producer envelope from the #1064 salvage branch into core
(basic_memory/hooks/): a frozen Envelope dataclass with the 2026-07-15
revision fields (UUIDv7 id, actor, caused_by, promotion_status), metadata-only
idempotency keying, and the provenance observation/frontmatter projections.
The factory redacts every payload, so nothing built through it can carry
unredacted values into the inbox.

The Stage-1 redaction floor combines detect-secrets scanning (known token
formats plus an entropy threshold, with the eager-scan entropy limit
re-applied) with the salvaged recursive deny-key / deny-path / env-pair /
truncation rules, separator-normalized for Windows and hardened to be pure
and idempotent. detect-secrets lands as a core dependency: its tree is light
(pyyaml already ships; requests is the only addition) and the floor must be
unconditionally present on the capture hot path — an optional extra would
make redaction availability configuration-dependent.

Includes a ~15-line RFC 9562 UUIDv7 generator; stdlib uuid.uuid7() exists
only on Python 3.14+, so swap when the floor rises.

Part of issue #997 (SPEC-55 revision 2026-07-15).

Co-authored-by: sourrrish <officialsourish0@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
The inbox is the SPEC-55 append-only WAL: one <uuid7>.json file per envelope
under <bm-home>/inbox/, written atomically (tmp + rename) so a crash can never
leave a half-written envelope visible. Filename order is capture order, so
consumers need no mtime/stat dependence. BM home resolves through core's
resolve_data_dir() — plugin directories are ephemeral and uninstalling a
plugin must never delete captured trace. Processed envelopes move to
processed/ for audit and are pruned after a retention window, with age derived
from the uuid7 timestamp in the filename; files that don't parse as UUIDs are
never deleted.

Adapters normalize each harness's hook stdin dialect (Claude Code per the
platform hooks reference and the shipped hook scripts; Codex per its hook
scripts) into one NormalizedHookEvent so everything downstream is
harness-agnostic. Realistic payloads are recorded as test fixtures.

Part of issue #997 (SPEC-55 revision 2026-07-15).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
bm hook flush's engine: sweep the whole inbox in filename order, group by
(source, source_session_id), and project SessionNote skeletons plus ToolLedger
entries through the same internal write path the CLI's write-note uses (the
MCP write_note tool via the async client — no subprocess).

Idempotent by construction: envelopes are hints, deduped on idempotency_key
within a sweep and against the processed audit trail, and artifacts are
re-derived with deterministic titles and overwrite=True — WAL replays and
duplicate hooks can never double-write. Artifacts carry created_by and
caused_by_event frontmatter plus the required [source] observation. Envelopes
without a resolvable project mapping stay pending (fail fast, never write to
the wrong project), as do groups whose write fails — both self-heal on the
next sweep. Corrupt envelope files are counted and left in place, never
deleted or guessed at.

Part of issue #997 (SPEC-55 revision 2026-07-15).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
The harness producer front door: plugins reduce to shims that exec
'bm hook <event> --harness claude|codex' with hook JSON on stdin.

session-start and pre-compact port the existing plugin hook behavior into
core — the session context brief (structured task/decision/session queries,
shared-project read set, placement guidance, nudges) printed to stdout under
the 10k char cap with graph-derived content fenced and labeled as reference
data, not instructions; and the extractive pre-compaction checkpoint note
written through the same internal path as bm tool write-note. Settings
resolution is ported from the hook scripts: the basicMemory block of the
nearest-ancestor .claude settings files over the user-level settings.json
for Claude, and .codex/basic-memory.json for Codex; --project-dir overrides
the payload cwd for project mapping.

When captureEvents is the JSON boolean true (fail-closed: strings never
enable), both verbs also capture a floor-redacted envelope into the inbox.
Harness verbs are fail-open: any error logs to stderr and exits 0, never
disrupting a session. flush runs the projector; status is the debuggability
surface (inbox depth, last flush, settings summary, bm/uv versions).

Part of issue #997 (SPEC-55 revision 2026-07-15).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
The plugin hook scripts are now zero-logic shims: resolve the Basic Memory
CLI (BM_BIN override -> basic-memory/bm on PATH -> uvx at a released floor)
and exec 'bm hook <event> --harness claude|codex' with the hook JSON passed
through on stdin. Nothing resolvable exits 0 silently (fail-open). The
Claude shims pass ${CLAUDE_PROJECT_DIR} as --project-dir so project mapping
never trusts cwd; Codex mapping keeps using the payload cwd.

The Codex standalone Python hook implementations are deleted — their logic
lives in basic_memory/hooks/ and the hook CLI since the core phase — and the
Codex validator drops the .py script requirements accordingly. hooks.json
manifests are unchanged (${CLAUDE_PLUGIN_ROOT}/${PLUGIN_ROOT} commands,
matchers, and timeouts preserved); both plugin layouts already conform
(.claude-plugin/ and .codex-plugin/ hold manifests only, components at root).

The shell-hook tests are rewritten as a shim e2e suite covering both
plugins: fake recording CLIs assert resolver order, stdin passthrough, the
uvx version floor, BM_BIN override semantics (spaced paths and multi-token
launchers), project-dir plumbing, and silent fail-open — keeping the Git
Bash Windows resolution pattern.

Part of #997 (SPEC-55).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
'bm hook install' wires the lifecycle hooks into the user-level harness
config for standalone (non-marketplace) users: the Claude Code settings
hooks block (~/.claude/settings.json) or a Codex hooks.json
(~/.codex/hooks.json) using the plugin's hooks.json schema and matchers.
Entries are ownership-tagged through their command shape (the codex-honcho
ownership-regex approach): 'bm hook remove' deletes exactly the entries
matching OWNED_HOOK_COMMAND_RE — surgical against pre-existing user hooks,
including mixed groups — and both verbs are idempotent. Malformed configs
fail fast instead of being clobbered. install also checks for uv and prints
a per-platform install hint when missing (warning only: a PATH-installed
basic-memory works without uv).

Also carries the Codex 'focus' config key into the unified session brief
header — the one codex-script feature the core-phase unification dropped.

Tests cover merge/remove against fixture configs with pre-existing user
hooks, idempotency both directions, fail-fast on malformed configs, the
per-platform uv hint, and the focus header.

Part of #997 (SPEC-55).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
scripts/update_versions.py now rewrites the 'basic-memory>=FLOOR' pin in
all four plugin hook shims alongside the manifests it already touches
(packages scope), so every release moves the uvx fallback to a version that
actually ships the 'bm hook' verbs. The floor keeps the Python version form
(e.g. 0.21.3b1) — it is a pip requirement spec, not the npm semver mapping.

Regression tests: packages scope bumps all four shims, core scope leaves
them untouched, and prereleases land in pip form.

Part of #997 (SPEC-55).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
… opt-in

Both plugin READMEs now list uv as the documented per-platform prerequisite,
disclose that the hook shims fall back to 'uvx basic-memory>=FLOOR' (network
fetch from PyPI on first run, floor bumped by release tooling), and document
the captureEvents gate as opt-in and off by default (JSON boolean true only).
settings.example.json and the Codex config example gain 'captureEvents':
false. CHANGELOG gains the Unreleased entry for the bm hook front door, and
the plugin CHANGELOG notes the shim reshape.

Part of #997 (SPEC-55).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
Five findings from the multi-lens review of the branch survived adversarial
verification and are fixed here:

- projector: rebuild session artifacts from the complete session (processed +
  fresh envelopes) so incremental flushes no longer erase events projected on
  an earlier sweep (was overwriting from still-pending envelopes only).
- projector: use the full session id in artifact titles; an 8-char prefix let
  distinct sessions collide on the same permalink and clobber each other.
- inbox: mark_processed tolerates an envelope already retired by a concurrent
  sweep instead of crashing the whole flush with FileNotFoundError.
- inbox/_uuid7: uuid7_unix_ms fails fast on non-v7 UUIDs so retention never
  derives a garbage capture time from a v1/v4 name and deletes it.
- hook: pre-compact checkpoint transcript excerpts now pass the Stage-1
  redaction floor (new redaction.redact_text) before entering the note title,
  summary, and observations — closing the #997 'redact before writing' gap.

Regression tests added for each finding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 63bc6c837d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/claude-code/hooks/session-start.sh Outdated
Comment thread src/basic_memory/hooks/redaction.py Outdated
…ows shim

Static Checks + Windows unit failures and the two Codex P2 findings:

- redaction: deny-path redaction now replaces the matching path token wherever
  it appears, not only when the whole value starts with the prefix. Free-text
  checkpoint excerpts such as "please read ~/.ssh/id_rsa" had the path left
  intact by redact_text(); the prefixes now compile to substring matchers that
  span either separator and consume the path tail. Whole-value paths still
  collapse to the marker, so existing behavior is preserved.

- shims: PATH-resolved basic-memory/bm are probed for `hook` support
  (`hook --help`, stdin detached) before they win over the uvx floor. A stale
  pre-hook install on PATH no longer shadows the floor and exec a CLI that
  errors — it falls back, keeping the fail-open contract. Applied to all four
  Claude/Codex shims.

- shims: the BM_BIN single-word test uses `-e` (exists) instead of `-x`
  (executable). Git Bash reports extensionless files as non-executable, so a
  spaced BM_BIN path was word-split on Windows, breaking
  test_bm_bin_executable_path_with_spaces_stays_one_word.

- test: narrow mock_write.await_args before .kwargs so `ty` stops flagging
  attribute access on `_Call | None` (matches the sibling assertions already in
  the file).

Regression tests added for embedded-path redaction (free text and payload
values) and the stale-install fallback (falls back to uvx; stays silent when
nothing else is resolvable).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

Addressed both P2 findings plus the Static Checks (ty) and Windows unit failures in 7fd1d03:

  • deny-path redaction is now substring-based (free-text checkpoint excerpts have embedded secret paths replaced in place)
  • all four shims probe hook support before a PATH install can shadow the uvx floor (stale-install fail-open)
  • BM_BIN single-word detection uses -e instead of -x (Git Bash reports extensionless files as non-executable on Windows)
  • narrowed mock_write.await_args before .kwargs for ty

All added regression tests, plus ruff, ty, and just package-check-claude-code/package-check-codex pass locally.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7fd1d03098

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/hooks/redaction.py Outdated
Comment thread plugins/claude-code/hooks/session-start.sh Outdated
Two follow-up P2 findings from the re-review of 7fd1d03:

- redaction: the deny-path floor only carried the *expanded* home paths
  (/home/.../.ssh/), so a literal "~/.ssh/id_rsa" — the form prose and
  transcript excerpts actually type — survived redact_text(). The default list
  now denies both the expanded and the literal "~/" prefixes, deduped.

- shims: the trailing `exec` tail-exec'd a resolved launcher's non-zero exit to
  the harness. A cold uvx that cannot reach PyPI, an unbuildable floor, or a bad
  BM_BIN would then disrupt the session — violating the "advisory, must NEVER
  disrupt" contract in the shim header. All four shims now run the command
  instead of exec-ing it and always exit 0; stdout still reaches the session
  context and stdin still passes through.

Regression tests: unexpanded-tilde redaction (free text + payload) and
fail-open on a launcher that errors at runtime (all four shims).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

Round-2 findings addressed in aacb9ab:

  • Literal ~/ deny paths: the default deny list now carries both the expanded home path and the unexpanded ~/ prefix, so prose/transcript references like ~/.ssh/id_rsa are redacted.
  • Shim fail-open: all four shims run the command instead of exec-ing it and always exit 0, so a resolved-but-failing launcher (cold offline uvx, unbuildable floor, bad BM_BIN) can't propagate a non-zero exit to the harness.

Regression tests added for both. ruff/ty/format clean; package-check-claude-code and package-check-codex pass; 185 hook-related tests green locally.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aacb9ab643

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/hooks/envelope.py Outdated
Comment thread src/basic_memory/cli/commands/hook.py Outdated
Two more P2 findings from the re-review of aacb9ab — the resolved hook cwd
bypassed the redaction floor:

- envelope: create_envelope only scrubbed `payload`; the top-level `cwd` was
  stored raw in the inbox WAL. A session under a configured redactPaths (or a
  default deny dir) now has its cwd run through redact_text before the Envelope
  is built. project_hint is a project name, not a path, and the projector
  resolves against it, so it is intentionally left intact.

- checkpoint: the pre-compact note wrote `event.cwd` verbatim into the
  frontmatter `cwd:` field and the "Working in ..." body line. It is now
  redacted once at extraction (alongside the transcript excerpts) so both draw
  from the scrubbed string. The functional uses of cwd (git status, project
  mapping) run earlier on the real path and are unaffected.

Regression tests: envelope cwd redaction (denied + ordinary) and checkpoint cwd
redaction under a configured redactPaths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

Round-3 findings addressed in 709a622 — the resolved hook cwd now passes the redaction floor:

  • Envelope cwd: create_envelope scrubs cwd via redact_text before building the Envelope, so a denied workspace path isn't retained in the inbox WAL.
  • Checkpoint cwd: the pre-compact note redacts event.cwd at extraction; both the frontmatter cwd: field and the body line use the scrubbed value.

Regression tests added; ruff/ty/format clean and 155 hook tests green locally.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 709a62237e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/hooks/projector.py Outdated
Comment thread src/basic_memory/cli/commands/hook.py
…(PR #1070 round-4)

Two P2 findings from the re-review of 709a622:

- projector: an in-group idempotency replay was retired to processed/ before the
  fresh envelope it duplicated was written. When that write failed, the next
  sweep read the replay's key from processed/ and retired the still-pending fresh
  envelope as a replay too — the session never projected, breaking the documented
  self-heal. Replays are now split: a replay of an already-projected envelope is
  retired immediately (its original is durable), but an in-group replay is
  retired only after its fresh sibling's write succeeds, so both self-heal
  together on a failed sweep.

- inbox: a session that never resolves a project mapping (primaryProject unset
  for its whole lifetime) produced envelopes the projector could never route and
  held pending forever — flush routes solely on the stored project_hint and never
  reloads settings. Retention now prunes pending envelopes on the same window it
  already applies to processed/, so unresolvable trace can't accumulate without
  limit. Capture-without-mapping is unchanged (a later same-session hinted
  capture still resolves the whole group via the merge); invalid entries are
  preserved so retention never eats the corruption trace `bm hook status`
  surfaces.

Regression tests: reproject-after-write-failure with an in-group replay; prune
of expired unresolved pending envelopes; recent unmapped pending kept.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

Round-4 findings addressed in e908d94:

  • Projector replay-retire race: in-group idempotency replays are now retired only after their fresh sibling's write succeeds, so a failed write self-heals both instead of stranding the session.
  • Unbounded unmapped inbox: retention now prunes pending envelopes on the same window as processed/, so a fully-unmapped session no longer stays pending indefinitely. Capture-without-mapping is preserved (deliberate + tested); invalid entries are kept for inspection.

Regression tests added; ruff/ty/format clean and 158 hook tests green locally.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e908d94366

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/hooks/projector.py Outdated
 round-5)

Follow-on from the round-4 replay fix. A same-minute replay retired into
processed/ lives next to its original (same idempotency key, distinct uuid7 id).
When a later distinct event triggers a full rebuild, the reconstruction merged
all processed envelopes back into the artifact without re-deduping — so the
SessionNote/ToolLedger rendered duplicate rows for the replayed event, violating
the idempotency contract.

The merged (prior + fresh) list is now deduped by idempotency key before
building the notes, keeping the earliest (chronological uuid7) envelope per key —
the original, not its replay.

Regression test: test_flush_dedups_retired_replay_history_on_rebuild.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

Round-5 finding addressed in c04e0c8: the projector now dedups the merged prior+fresh envelope list by idempotency key before rebuilding, so a replay retired into processed/ next to its original no longer produces duplicate rows in the SessionNote/ToolLedger. Regression test added; ruff/ty/format clean and 159 hook tests green locally.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c04e0c869a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/hooks/projector.py Outdated
 round-6)

Follow-on from the round-4/5 replay handling. When two same-minute captures of
the same event share an idempotency key and the mapping was set only for the
later one (primaryProject configured between the two hooks), the first envelope
is fresh with an empty project_hint and the second lands in group_replays. The
routing scan ran over the deduped fresh+prior list only, so it missed the
replay's hint, left the whole group pending, and retention could eventually
prune trace that actually had a valid mapping.

Rendered rows still dedup (one per key), but routing now scans the complete
group including the in-group replays, so a hint carried only by a replay still
routes the group.

Regression test: test_flush_routes_on_project_hint_from_in_group_replay.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

Round-6 finding addressed in 0a39f5f: the projector's project-hint routing now scans the complete group (fresh + prior + in-group replays), so a mapping carried only by a same-minute replay routes the group rather than leaving it pending. Rendered event rows still dedup by key. Regression test added; ruff/ty/format clean, 160 hook tests green locally.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0a39f5fd06

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/cli/commands/hook.py Outdated
Comment thread src/basic_memory/hooks/projector.py Outdated
…files (PR #1070 round-7)

Two P2 findings from the re-review of 0a39f5f:

- install: `bm hook install` always wrote a bare `basic-memory hook ...` command.
  A standalone user with uv but no persistent basic-memory/bm on PATH (installed
  via `uvx basic-memory hook install`) would then hit command-not-found at
  SessionStart/PreCompact. Install now resolves the launcher like the shims do —
  basic-memory / bm on PATH, else a `uvx "basic-memory>=<floor>"` fallback pinned
  to the running release floor — and writes that. The ownership regex now keys on
  the `hook <verb> --harness <harness>` suffix so `remove` still recognizes every
  launcher form (including the uvx one). With nothing resolvable it still writes
  the basic-memory form and warns about the missing uv (unchanged).

- flush: the inbox load loop only caught JSON/shape errors, so a pending file
  vanishing between list_envelopes() and read_text() (overlapping flushes) or a
  transiently unreadable file raised OSError and aborted the whole sweep —
  skipping valid envelopes and the flush marker. OSError is now caught and the
  entry skipped (not counted invalid, since it isn't corrupt trace).

Regression tests: install resolves uvx / bm launchers and remove strips the uvx
form; flush skips a vanished inbox file and still projects + records the marker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

Round-7 findings addressed in 3d6e78a:

  • install resolvable command: install now resolves basic-memory/bm on PATH, else a pinned uvx fallback, so a uvx-only standalone user gets a hook command that resolves at runtime. Ownership regex broadened to the hook <verb> --harness suffix so remove still works for every launcher form.
  • flush tolerance: the sweep now catches OSError around read_text, so a vanished/locked inbox file is skipped instead of aborting the whole flush.

Regression tests added; ruff/ty/format clean, 163 hook tests green locally.

…1070)

The projector hand-built the artifact frontmatter as a `---` text block. One of
its fields, `envelope_turn_id`, comes from `source_turn_id`, which SPEC-55
defines as opaque, surface-defined text. A value with YAML-special content —
`turn: 42`, a stray colon, a newline — produced an invalid frontmatter block,
so write_note failed to persist the artifact and the whole session stayed
pending on every flush retry (the envelope shape is otherwise valid, so it never
ages out as invalid either).

This is the same failure the pre-compact checkpoint hit and fixed in round-21:
build a `metadata` dict and pass it through `write_note(metadata=...)`, which
serializes/quotes values safely, instead of splicing raw text into a hand-built
block. `_artifact_metadata` now returns that dict (`created_by`,
`caused_by_event`, plus `to_frontmatter_fields`, and `status: open` for the
session note); `_session_note`/`_tool_ledger_note` return
`(title, body, metadata)`; `_write_artifact` forwards it. The note `type` still
rides the `note_type` arg. Body content no longer carries a frontmatter block.

Regression test: an envelope whose `source_turn_id` contains `turn: 42\n...`
flushes successfully, with the raw value reaching write_note as a structured
metadata value rather than spliced into the body.

Reported by Codex on PR #1070 (projector.py:140).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 64b7f2e4fb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/hooks/projector.py
…(PR #1070)

Two overlapping `bm hook flush` runs could race: flush B lists {B, C}, writes
the full artifact, and retires C to processed/; a stale flush A (snapshot {B})
then overwrites the SessionNote/ToolLedger from {A, B} with overwrite=True,
dropping C's row. Because C is already retired, a later sweep with no fresh
pending in that group never rebuilds the artifact, so C stays missing until
another event arrives in the session (the envelope trace itself is safe in
processed/ — it's the rendered artifact that loses the row).

flush() now runs under an exclusive advisory lock over the inbox
(`inbox.flush_lock()`, via filelock — fcntl on POSIX, msvcrt on Windows). A
flush that arrives while the lock is held skips rather than blocks: the running
flush sweeps the whole inbox, so the skipped run loses no work, and skipping
avoids exactly the stale overwrite. The lock releases on process death, so a
crashed flush strands nothing. The flush body moved to _flush_locked; a new
FlushResult.skipped flag is surfaced by `bm hook flush`.

filelock was already resolved transitively (via huggingface-hub); declared as a
direct dependency now that core imports it.

Regression test: with the inbox lock held, flush() skips — writes nothing,
leaves the envelope pending for the next unlocked sweep.

Reported by Codex on PR #1070 (projector.py:360).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: 14b07c7d6f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@phernandez

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 14b07c7d6f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/cli/commands/hook.py
… (PR #1070)

When a Codex checkpoint runs from a directory covered by `redactPaths`, the cwd
is redacted — but the "## Working tree" section rendered raw `git status
--short` rows, which are repo-relative filenames (`M customer-roadmap.md`) with
no absolute prefix. Per-row `redact_text` can't match the deny path against a
bare relative filename, so the denied workspace's file list leaked into the
checkpoint, defeating the user's redaction intent.

Skip the working-tree section entirely when `safe_cwd == REDACTED_PATH` (the cwd
matched a redactPaths entry, so the whole workspace is denied and every row is
sensitive). Rows are still per-row redacted when the workspace is not denied, to
catch a secret in a filename or an absolute denied path appearing in a row. Only
the Codex path renders this section (include_workspace_sections).

Regression test: a codex pre-compact whose cwd is under redactPaths omits the
working-tree section and the filenames, while the cwd stays redacted in
frontmatter.

Reported by Codex on PR #1070 (hook.py:729).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d651ba2e8b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/hooks/redaction.py
phernandez and others added 2 commits July 20, 2026 10:35
…e (PR #1070)

_write_hook_config used a bare write_text over ~/.claude/settings.json /
.codex/hooks.json — the user's entire harness config, not just our
entries — so a crash mid-write could truncate it to a partial JSON
document. Write tmp + os.replace instead, mirroring the inbox WAL
pattern, and pin the crash case with a regression test that fails the
tmp write and asserts the original file survives intact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
…(PR #1070)

Marketplace plugin installs are git clones, and Git for Windows defaults
to core.autocrlf=true: without an eol pin the hook shims check out with
CRLF endings and bash exits 2 with syntax errors on every session start,
the one exit code Claude Code surfaces to the user — breaking the
fail-open contract for the whole Windows audience. All tracked .sh files
are already LF in the index, so this only pins checkout behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6cd43b7210

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/cli/commands/hook.py Outdated
#1070)

The tmp + os.replace rewrite introduced in 5d61444 published the tmp
file's umask-default mode, silently widening a deliberately private
(0600) settings file to 0644. Create the tmp at the original's mode via
os.open (O_CREAT's mode is umask-masked, so the tmp is never observable
wider than the original), then chmod to the exact mode before replace.
A missing target keeps umask defaults, matching a plain write. Pinned
by a POSIX-only regression test; the crash-safety test now injects its
failure through the os.fdopen handle the rewrite actually uses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c0bc0abfa6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/cli/commands/hook.py Outdated
)

O_CREAT's mode argument applies only at creation, so a stale tmp left
by a crashed earlier run kept its old (possibly world-readable) mode
and private config content was written into it before the exact-mode
chmod. Unlink any stale tmp and open with O_EXCL so the tmp is always
freshly created at the intended mode. Pinned by a regression test that
seeds a 0644 stale tmp beside a 0600 settings file and asserts the fd's
mode is 0600 at open time, before any content is written.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3db833e451

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/hooks/envelope.py Outdated
…s (PR #1070)

Identity fields (source_session_id, source_turn_id) are opaque
surface-defined strings that skip the payload redaction floor, so a
newline-carrying id — hostile hook stdin or a corrupt replayed inbox
file — became extra parsed observation/relation lines in the projected
SessionNote body, the same injection class the frontmatter path already
blocks via metadata= serialization. Collapse control characters in every
interpolated field at emission, which covers replayed envelopes that
never pass through the capture factory. Pinned by a regression test
injecting observation and relation lines through both ids.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 123a44fc0e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/hooks/projector.py Outdated
phernandez and others added 2 commits July 20, 2026 13:28
The four shell shims were 68 lines each of resolver edge cases — Git
Bash -x lies, word-splitting, quote-stripping, stdin-detached probes,
CRLF fragility — to solve a problem uv already solves. Each hook is now
a self-contained PEP 723 script run via uv run --quiet --script: the
inline metadata pins basic-memory>=<floor>, uv resolves and caches the
environment, and the script invokes the Typer hook command in-process.
BM_BIN (a path or quoted launcher string) remains the development
override. Fail-open is a single guard: every failure path exits 0.

This deletes the resolver logic outright — no PATH probing, no
stale-binary hook --help probe, no uvx fallback ladder — and makes the
hooks native on Windows (no bash, no CRLF exposure). uv becomes a hard
prerequisite, already documented for the former fallback path.

Each script gets a co-located test file in the same directory (pytest
testpaths now includes plugins/) executing the real launcher via
sys.executable: BM_BIN argv/stdin passthrough, launcher quoting,
project-dir plumbing (claude) or its absence (codex), fail-open on an
unresolvable BM_BIN, the in-process CLI path against an empty HOME, and
the PEP 723 floor pinned to __version__.

Release tooling bumps the floor on the anchored dependencies line
(HOOK_SCRIPTS in update_versions.py), which also retires the
two-match regex hazard flagged in review; the updater tests seed the
realistic script shape. Both plugin validators now require the PEP 723
floor. Central plugin tests pin the hooks.json uv run wiring, shebangs,
exec bits, and that no .sh shims reappear. Docs and unreleased
changelog entries updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
…#1070)

The provenance-observation fix (123a44f) collapsed control characters
in to_provenance_observations but missed the sibling emission sites:
note titles, SessionNote Events lines, ToolLedger Entries lines, and
the ledger's [source] observation all interpolate the raw session id or
replayed envelope fields, so a newline-carrying value could still parse
as injected observations/relations. The sanitizer is now exported as
single_line and applied to every dynamic line both note builders emit.
Regression test drives a newline-injecting session id through flush and
asserts neither artifact carries the injected lines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f7dd1972df

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/claude-code/hooks/hooks.json
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

@phernandez
phernandez merged commit f48ff25 into main Jul 20, 2026
34 checks passed
@phernandez
phernandez deleted the feat/997-bm-hook-front-door branch July 20, 2026 20:05
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: f7dd1972df

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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.

Capture Claude/Codex harness events as Basic Memory producer envelopes

1 participant