Commit Graph
11 Commits
Author SHA1 Message Date
Colby MchenryandClaude Opus 5.5 c1744e7e76 fix(prompt-hook): desktop reminders and other sessions' messages no longer drive the hook (#2798)
Claude Code's desktop app puts <system-reminder> blocks (which local
sessions are linked, and similar status) in front of the prompt it
submits, and UserPromptSubmit hooks receive them inside `prompt`. The
hook gated, picked the project and explored on the whole string, so the
reminder's words drove it: a linked session titled after its work, like
"trace-order-flow", carries structural keywords, so every turn, a bare
"yes" included, got a full explore of the reminder. A reminder in front
of a task notification or a subagent hand-back also defeated the
start-anchored envelope checks, and a message from another session
(a <cross-session-message from=...> envelope) was never skipped at all.

The hook now removes every complete <system-reminder> block before the
envelope checks, the gate, project selection and explore, returns early
when nothing is left, and skips a prompt that is entirely a
<cross-session-message> envelope, as it already did for task
notifications and subagent hand-backs. The scan stops at the last
closing tag and the attribute run stops at `<`, so a pasted prompt full
of unclosed tags stays linear (520 KB in under 1 ms; the `[^>]*` form
took 83 ms on a tenth of that and grows quadratically).

Validation:
- Prompt shapes come from the Claude Code desktop transcripts on this
  machine (314 transcripts): reminders always lead, one to four of
  them, separated by one or two newlines, and cross-session messages
  follow one to four reminders.
- frontload-hook.test.ts: 15 new tests (8 spawn the built hook with
  Claude Code-shaped payloads) fail on main and pass with the fix; the
  file's 48 other tests pass on both.
- Built CLI, private HOME, 13 payloads: on main every prompt behind a
  reminder got a full explore of the reminder (2.1 to 5.0 KB); with the
  fix the 8 that are not user input inject nothing and exit in about
  200 ms instead of 680 ms, and a question behind reminders injects
  byte-for-byte what the same question alone does.

Issue #2635. Contributor PR #2642 (yang0228) was read for guidance.

Fixes #2635

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 11:44:29 +00:00
96ba880556 fix(prompt-hook): skip Claude Code <agent-message> subagent hand-backs (#2184) (#2281)
* fix(prompt-hook): skip Claude Code <agent-message> subagent hand-backs

Claude Code delivers a subagent's report to the parent session as a
UserPromptSubmit prompt wrapped in <agent-message from="…">…</agent-message>.
The prompt hook treated it as user input and ran a full codegraph_explore
on the whole report, which is slow on a multi-KB prose query (past the
30 s hook timeout in the report) and injects context unrelated to the
user's question.

Add isAgentMessage next to isTaskNotification (#1832) with the same rule:
only a prompt that is entirely the envelope is skipped, so a user question
that mentions the tag still goes through the gate.

Fixes #2184

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 240022db70)

* docs(changelog): credit the prompt-hook hand-back fix

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:12:11 +00:00
ee97a57a9b fix(directory): a schema-less codegraph.db does not make a project initialized (#1895) (#2083)
* fix(directory): a schema-less codegraph.db does not make a project initialized (#1895)

isInitialized() accepted any existing .codegraph/codegraph.db, so an empty or
table-less file in an ancestor directory (an interrupted init, a stray touch,
a never-populated ~/.codegraph/) captured the upward walk of
findNearestCodeGraphRoot / resolveProjectPath / resolveServerRoot for every
project beneath it: the real index of a sub-project became unreachable, the
sub-project down-scan never ran, and commands either operated on the broken
file or failed opening it instead of showing the not-initialized guidance.

An initialized project is now one whose db carries the schema: the probe is
gated by file size (>= the 100-byte SQLite header), then the header magic,
then a read-only node:sqlite open with a single sqlite_master lookup for the
`nodes` table, memoized per path + mtime + size so hot callers (the prompt
hook, MCP root resolution on every call) never reopen an unchanged db. The
handle is closed immediately so the probe never holds the db on Windows; a
locked/busy db counts as live.

createDirectory() uses the same predicate, so `codegraph init` in the
directory with the broken file repairs it in place (the schema is applied
with CREATE ... IF NOT EXISTS) instead of refusing with "Already
initialized"; the CLI says what it found. Nothing is deleted automatically.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(directory): a database we cannot open read-only still counts as initialized

The schema probe failed CLOSED: a real WAL codegraph.db in a directory the
process cannot write (a read-only checkout, a mount, another user's tree)
made the read-only node:sqlite open throw — SQLite must create `-shm` to
open a WAL database — and isInitialized() reported the project as not
initialized, where main's file-exists check had been right.

The probe now fails OPEN. Once the size gate and the `SQLite format 3\0`
header have passed, the file is a SQLite database; only a SUCCESSFUL
sqlite_master query proving the `nodes` table absent may return false. Any
open/prepare error (locked, busy, cantopen, readonly, disk I/O) returns
true, the pre-existing behaviour for a database we cannot inspect. No error
strings are matched. The per path + mtime + size memo is unchanged, and
schema-less/empty files still return false, so `codegraph init` still
repairs them and never touches a real db.

Test: a WAL db copied into a `.codegraph/` made read-only with chmod stays
initialized — skipped on win32 and when running as root (root ignores mode
bits).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(init): refuse a codegraph.db that is not SQLite instead of promising a rebuild

Review found that a `.codegraph/codegraph.db` holding bytes that are not
SQLite at all counted as "schema-less", so `init` printed "rebuilding it"
and then failed with SQLite's "file is not a database". Only an empty file
or a SQLite database without the codegraph tables can be rebuilt in place;
`hasSchemalessDb` now means exactly that, and the new `hasForeignDbFile`
names the other case. `init` stops on it with exit 1, says which file it
is and to move or delete it, and never touches it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(directory): ask SQLite what codegraph.db is instead of reading its header ourselves (#1895)

The header check opened and closed a descriptor of our own on
`codegraph.db`. Closing any descriptor on a database file drops every
POSIX lock the process holds on it, including those of a connection it
already has open (sqlite.org/howtocorrupt.html §2.2.1) — and the MCP
server resolves projects through isInitialized on every call while it
holds the index as its writer. mcp-projectpath-lifecycle's takeover tests
caught it: after a real owner exits, the taking-over engine's next read
failed with "disk I/O error" (2 of 21 fail with the header read; 21/21
on main and with this change).

One read-only SQLite probe now answers all three questions: the schema
is there, SQLite opened it and the schema is not (repairable), or SQLite
refused it as not a database (SQLITE_NOTADB). Anything else — locked,
busy, no `-shm` possible — still fails open as initialized. SQLite's own
connections share one lock table per file, so opening and closing a
second one leaves the first one's locks alone. A test pins that
isInitialized and both init helpers never open the file themselves while
this process holds it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(changelog): keep the #1895 entry clear of #1917's, so the carries merge in any order

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-29 03:01:50 +00:00
Colby MchenryandClaude Opus 5.5 4fd3e6d86b fix(prompt-hook): require corroboration for ambiguous words (#1654) (#2012)
Everyday uses of como and wie were treated as sufficient structural evidence for HIGH.
Remove these words from the unconditional keyword list so existing index verification determines HIGH, MEDIUM, or silence.
Add real-index CLI regressions while preserving existing English and multilingual structural tests.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:06:42 +00:00
cba44c9cad fix(prompt-hook): skip Claude Code task notifications (#1832) (#1884)
Claude Code's UserPromptSubmit hook also fires for the
`<task-notification>…</task-notification>` blocks the host injects as
`user` messages when a background agent finishes. Those blocks are long
machine text full of identifier-shaped tokens, so they pass the structural
gate and the whole blob is sent to codegraph_explore as the query — a
3.5s median (7.4s max) stall on the blocking path, on text the user never
typed.

Recognize a prompt that consists solely of one task-notification envelope
(leading/trailing whitespace allowed) and exit before any project lookup,
gate or explore work. A prompt that merely mentions the marker, or has
text around it, is still treated as a user prompt.

Fixes #1832

Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-27 12:22:37 +00:00
Colby MchenryandColby McHenry 3193800bc8 fix(prompt-hook): skip unsafe roots during subproject down-scan (#1454) (#1812)
Reuse unsafeIndexRootReason before scanning indexed subprojects so stray manifests at home or broader roots cannot inject unrelated context. Preserve workspace adoption for #964.

Validation: four new regressions fail before the guard and pass after it; 48 relevant tests and npm run build pass. Confirmed the real os.homedir() leak before and after the fix with fixture cleanup.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 20:41:32 -05:00
Colby MchenryandColby McHenry 4105249843 fix(prompt-hook): cap injection under Claude Code's 10k inline limit (#1694) (#1769)
Claude Code persists hook stdout over 10,000 characters to a file and shows
the model a 2 KB preview. The prompt-hook MAX of 16,000 always hit that path
once explore filled the budget. Cap at 9,000 (exported + unit-tested) so the
payload lands inline, with headroom for the wrapper and projectPath nudges.

Lands the approach from #1695 with a testable helper.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 09:15:13 -05:00
Colby MchenryandClaude Fable 5 713ab7af43 fix(prompt-hook): bound the call/trace/affect/connect stems on the right so ordinary words can't fire the gate (#1138) (#1147)
The multilingual structural-question gate (#1134) matches stems as open
prefixes (left boundary only) so derived forms fire without enumeration.
Four English stems have common non-structural completions — callus,
calligraphy, Connecticut, connective, affectionate, Tracey — that
false-fired the HIGH (full-explore) tier. Those four now enumerate their
structural suffixes and re-assert the right boundary; callbacks/callable/
call sites are included so no structural form regresses. Also documents
the verified-unfixable Korean homograph class on the unsegmented table
(#1140): segmentation can't split 구조대 from 구조가, and a denylist would
break 구조대로.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 17:05:51 -05:00
Colby MchenryandClaude Fable 5 317e7f4d3d fix(prompt-hook): make the structural-question gate multilingual (#1126) (#1134)
* fix(prompt-hook): fire the structural gate for Latin-script, Cyrillic, and JA/KO prompts (#1126)

The prompt-hook's keyword gate only knew English and simplified-Chinese
keywords, so a structural question in French (or Spanish, German, Italian,
Portuguese, Russian, Japanese, Korean, traditional Chinese) silently
no-op'd unless it happened to contain an identifier-shaped code token —
the #994 symptom, resurfaced for every other language.

Root causes fixed:
- JS \b is ASCII-only: a keyword whose first/last char is accented or
  non-Latin (où, qué, Cyrillic, kana) can never match \bkeyword\b —
  the same mechanism behind #994. Keyword matching now uses Unicode
  lookaround boundaries ((?<![\p{L}\p{N}_]) … (?![\p{L}\p{N}_])).
- Bare-stem English entries never matched their own derived forms
  (\barchitect\b can't match "architecture", \bdepend\b can't match
  "dependencies"). Stems are now matched as word prefixes (leading
  boundary only), which also lets one shared stem cover the Romance/
  Germanic spellings that coincide.
- The "CJK" set was simplified-Chinese-only: Japanese (呼び出し, 仕組み,
  実装 — and 追跡 ≠ 追踪), Korean, and traditional-Chinese terms are now
  in the unsegmented substring set.

Code-token extraction and the graph-verification path are unchanged;
non-structural prose stays a zero-cost no-op in every language.

Fixes #1126

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(prompt-hook): extend the gate to tier-2 languages (VI/TR/ID/PL/UA/NL/CS/RO/HU/EL/Nordics/FI/HI/AR/FA/HE/TH)

The first pass covered the 10 largest languages; this closes the rest of
the major-developer-population set (~29 total). Notable per-language
mechanics the curation had to respect:

- Agglutinative languages (Turkish, Finnish, Hungarian) need stems, not
  exact words — suffixes attach to everything (akışı, riippuu, működik).
- Indonesian me-/di-/ber- prefixes block leading-boundary stems, so
  affixed forms are listed explicitly (memanggil, dipanggil, berfungsi).
- Arabic/Farsi/Hebrew are spaced but proclitics attach to the word
  (وكيف = and-how), so they join the substring class with Thai.
- Ukrainian і/и spellings diverge from Russian (архітектур ≠ архитектур).
- Excluded terms that collide with English or code words: NL "pad",
  SV "var", CS "tok", Catalan "com" (matches every .com domain) — with
  regression tests pinning the exclusions.

Vietnamese was the sharpest gap: spaced Latin with heavy diacritics —
exactly the ASCII-\b failure class #1126 reports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 14:34:17 -05:00
Colby MchenryandClaude Opus 4.8 b45f309a1b fix(prompt-hook): fire front-load hook for non-English prompts (#994) (#1004)
The UserPromptSubmit hook's structural-prompt gate was English-only, so a
structural question written in Chinese — or any non-Latin script — silently
injected nothing: JS `\b` is ASCII-only and never matches between Han
characters, so the keyword regex couldn't fire (and couldn't be extended in
place). To the user the hook looked unwired, with no error to explain why.

Make the gate language-aware, split into tested helpers in directory.ts:
- hasStructuralKeyword: English (\b-guarded) + CJK structural keywords.
- extractCodeTokens: identifier-shaped tokens (camelCase / snake_case /
  name() / a.b) in any language — verified against the index via
  getNodesByName before firing, so a tech brand like `JavaScript` that looks
  like a symbol but isn't one here doesn't inject ~16KB of spurious context.
- isStructuralPrompt: the cheap candidate gate (keyword OR code-token).

Adds 21 unit tests for the gate (previously untested) covering the reporter's
verification table plus the false-positive guards.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:39:34 -05:00
Colby MchenryandClaude Opus 4.8 85a8f32fd9 fix(mcp): serve tools without a root index + make the front-load hook monorepo-aware (#964) (#966)
The MCP server gated tool availability on whether the server root had a
.codegraph/ index, so in a monorepo where only sub-projects are indexed the
agent saw zero tools — and couldn't reach an indexed sub-project even by
projectPath. A session started before `codegraph init` also never surfaced the
tools afterward. The Claude front-load hook had the mirror gap: it only walked
UP for an index, so it stayed silent at a monorepo root.

MCP server:
- Always expose the tool surface; when the root isn't indexed, send a
  per-project instructions variant (pass projectPath) instead of the
  "inactive" note. Safety comes from response SHAPE (success-shaped guidance,
  never isError), not from hiding tools.
- Reword the no-default-project guidance to be per-project, not per-session,
  and sharpen the projectPath schema description.

Front-load hook (UserPromptSubmit):
- Scan DOWN (bounded depth, workspace-root-gated) for indexed sub-projects and
  shape the injection by topology: front-load the one the prompt names, nudge
  about the rest, or list them when ambiguous.

Verified: full suite (1703 passed); a live two-package monorepo run confirms the
hook front-loads the correct sub-project with no cross-package leakage. The
front-load's net speed effect is the existing multi-file-vs-single-file
tradeoff, unchanged by this work.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:57:47 -05:00