- Feat: opt-in strict PreToolUse hook that actually makes agents use the graph. The installed Claude Code hook has always nudged the agent to run
graphify querybefore reading raw files, but a nudge is advisoryadditionalContextthe model routinely walks past mid-task.graphify install --project --strict(orgraphify claude install --strict) now installs a hook that blocks the first raw source read of a session (permissionDecision: "deny") with a redirect tographify query, then downgrades to the soft nudge — so it fires at most once per session and can never strand the agent (the next read proceeds even if no query ran, or ifgraphify queryitself failed). Running anygraphify query/explain/pathrefreshes a short-lived "recently oriented" stamp that suppresses the block. Strict mode is Claude Code only (Bash-grep and Glob stay nudge-only; Gemini/Codex/OpenCode can't hard-block and are unchanged);GRAPHIFY_HOOK_STRICT=1/0toggles it at runtime without a reinstall. Default installs are unchanged (soft nudge). - Fix: the PreToolUse hook stops crying wolf (#1840), which applies to the default soft nudge too. It no longer fires for reads of files outside the indexed project (a common false trigger, e.g. a
~/.claude/.../SKILL.mdread), and when the graph is stale for the target file (the file changed after the last build, orgraphify watchflagged the tree) it softens to a non-mandatory nudge that suggestsgraphify updateinstead of demanding the query. Gating is ~3statcalls — no corpus walk — so it stays fast on large monorepos, and fails open on any error. - Fix: a same-basename cross-extension re-export no longer manufactures a phantom self-cycle (#1814, thanks @Greg-Moskalenko for the report and @alphanury for the fix). A typed
.tswrapper that re-exports a hand-written.mjsruntime (export { N } from "./foo.mjs") hadfoo.tsandfoo.mjscollapse onto one base file id (the id stem drops the extension), and while_disambiguate_colliding_node_idscorrectly salts the two file nodes apart (foo_ts_foo/foo_mjs_foo), the re-export edge keyed its target salt by the importer's own source file — mis-pointing the./foo.mjstarget back atfoo.ts, asource == targetself-loop reported as a 1-file import cycle inGRAPH_REPORT.md. During disambiguation an import/re-export edge now carries the resolved target file as a transient salt key, so the salt lands on the real sibling node (generalizing the C/ObjC.h-sibling carve-out from #1475 to every language and tore_exports) and the phantom cycle disappears. That hint has no downstream reader and holds an absolute path, so it is popped once consumed and never persisted — and the graph serializer drops it as a backstop — keeping graph.json deterministic and byte-identical across checkout locations. Node ids are unchanged (the residual was purely at the edge layer). One caveat: a graph written by a pre-fix build still records the stale self-loop, and becausegraphify updateonly re-extracts changed files, an unchanged wrapper keeps that edge until it is next edited or a--forcefull rebuild runs — though any stale absolute hint a pre-fix graph happened to persist is dropped on the next build regardless. (The extension-aware-id alternative was rejected: it would rewrite every file and symbol id and force a full-rebuild migration in lockstep with the skill/validation id spec, #1033.) - Fix:
file_hash's stat-index memo is now keyed by the path salt, not the absolute path alone (#1989). The digest salts content with the file's path relative to the scan root (for cache portability), but the memo returned whichever digest was computed first for a given absolute path — so the same file hashed under two different roots (which happens within one--outrun) got an order-dependent result, and the wrong digest was persisted intostat-index.jsonacross runs. Each entry now stores one digest per salt; legacy un-salted entries are recomputed rather than trusted. Digest computation is unchanged, so existing cache entries still hit. - Fix:
--no-gitignoreextraction opt-out for projects that keep useful code under.gitignore(#1971, thanks @JensD-git for the report and @Mzt00 for the fix). The flag disables only VCS ignore rules (.gitignore+$GIT_DIR/info/exclude);.graphifyignore, the sensitive-dir/secret screens (#1666/#1943), and noise-dir pruning all still apply, so.git/,node_modules/, and real secrets stay out of the graph. The setting persists acrossupdate/watch/hook rebuilds and is no longer clobbered back on by a later flag-lessgraphify extract. - Fix:
.graphifyignore/.gitignoreglob matching now keeps*within a single path segment (#1975, thanks @oleksii-tumanov). An anchored*used to cross/(hand-rolled fnmatch), so anexclude-all + re-include subtreepattern collapsed to zero files and/src/*.pywrongly ignored nested files. Matching now follows git segment semantics (*per segment,**spans segments,dir/matches directories not same-named files), verified againstgit check-ignore; composes with the #1873 anchor-scoping and #1922 diagnostic. - Fix: DreamMaker parent-relative
#includepaths are no longer mangled (#1978, thanks @Osamaali313). The extractor usedstr.lstrip("./")— a character-set strip that ate every leading.//, so../shared/base.dmbecameshared/base.dm(and.hidden/xlost its dot), breaking include resolution. It now strips only a leading./prefix. - Fix:
graphify prsdecodesgh/git/claudeoutput as UTF-8 instead of the Windows locale codec (#1980, thanks @luke J). On Windowstext=Truedecoded child output as cp1252, mojibaking or crashing on emoji / non-ASCII names in PR titles and diffs; all subprocess reads now passencoding="utf-8", errors="replace"(matching the #1505 precedent), which also fixes the encode side when feeding a non-ASCII prompt to the claude-cli backend.