Context
The MCP-server instructions block already declares:
REQUIRED before Edit/Write on any file: get_files_context({ filepaths })
REQUIRED before renaming/removing/changing the signature of any exported symbol: get_dependents({ filepath, symbol })
In practice these are advisory — the model frequently goes straight to Edit/Write. A PreToolUse hook shipped with the plugin converts the "REQUIRED" wording into an actual gate, with zero per-user ~/.claude/settings.json wiring.
The intent is "did you do impact analysis on this file?", not "did you call this specific tool?" — three Lien tools can satisfy it.
Proposal
Ship a plugin-level hook bundle (PreToolUse + PostToolUse + SessionStart) that:
- On
Edit/Write/MultiEdit, reads tool_input.file_path and session_id from stdin.
- Skips if extension is outside Lien's indexed-extension set (no friction on
.md/.yml/.json).
- Looks up a session sentinel under
~/.lien/indices/<repoId>/gate-sessions/<session_id>/:
- existing-file edit: any of
fc-<hash> / dep-<hash> / fs-<hash> for that path
- new-file write: any recent
fs-* (loose by design)
- Applies a TTL (default 60 min) so stale sentinels don't satisfy the gate.
- If unsatisfied → emits
systemMessage naming the call that would satisfy it; advisory by default (exit 0 + stderr), blocking opt-in via plugin setting.
PostToolUse matched against mcp__plugin_lien_lien__(get_files_context|get_dependents|find_similar) writes the sentinels. SessionStart garbage-collects any gate-sessions/<id>/ whose id ≠ the current one.
Decisions
- Storage path —
~/.lien/indices/<repoId>/gate-sessions/<session_id>/. repoId derives deterministically via extractRepoId(cwd) from @liendev/parser (<basename>-<md5(path)[:8]>). Reuses the same path the vectorDB already creates. Will add lien path --store (one-line subcommand) so the shell script doesn't reimplement repoId derivation.
- Scope — per-repo, keyed off cwd. Matches index scope; session sub-dirs avoid cross-session bleed.
- Default mode — advisory (exit 0,
systemMessage only). Blocking is one config flag away. Rationale: a noisy first release will get the plugin uninstalled; advisory still reaches the model via systemMessage.
- Override —
LIEN_GATE=off env var (one-shot for the next call) plus a /lien-gate off slash command shipped with the plugin for the duration of the session. No magic comments (fragile).
- v2 deferred — symbol-level demand on
Edit (get_dependents for exported identifiers in old_str); tightening of find_similar looseness for new files.
v1 scope (PR-ready)
plugins/claude/hooks/hooks.json registering PreToolUse (matcher Edit|Write|MultiEdit), PostToolUse (matcher mcp__plugin_lien_lien__(get_files_context|get_dependents|find_similar)), SessionStart.
plugins/claude/hooks/gate.sh — PreToolUse logic; reads tool_input.file_path + session_id; respects LIEN_GATE=off.
plugins/claude/hooks/sentinel.sh — PostToolUse; parses tool_input (and for find_similar, scans tool_result JSON for cited filepaths).
plugins/claude/hooks/session-clean.sh — SessionStart; removes stale gate-sessions/<id>/.
lien path --store and lien path --extensions CLI subcommands.
Verified against installed-plugin examples
- Plugins can ship hooks (
hooks key in plugin.json or hooks/hooks.json wrapper). ${CLAUDE_PLUGIN_ROOT} resolves at runtime.
SessionStart/SessionEnd are both valid events.
PreToolUse payload contains session_id, cwd, tool_name, tool_input.file_path.
PostToolUse matchers handle mcp__* regex (see security-guidance and hookify plugins).
Simpler alternatives considered
- Sharper MCP instructions — already tried; model still skips. Rejected as standalone, kept as a parallel improvement.
- Status-line nag — visible to the human, not the model. Doesn't change behavior on the next tool call. Useful adjacent; not a substitute.
Stop hook reminder — fires too late; the edits already shipped.
Risks & escape hatches
Related
Context
The MCP-server instructions block already declares:
In practice these are advisory — the model frequently goes straight to
Edit/Write. APreToolUsehook shipped with the plugin converts the "REQUIRED" wording into an actual gate, with zero per-user~/.claude/settings.jsonwiring.The intent is "did you do impact analysis on this file?", not "did you call this specific tool?" — three Lien tools can satisfy it.
Proposal
Ship a plugin-level hook bundle (PreToolUse + PostToolUse + SessionStart) that:
Edit/Write/MultiEdit, readstool_input.file_pathandsession_idfrom stdin..md/.yml/.json).~/.lien/indices/<repoId>/gate-sessions/<session_id>/:fc-<hash>/dep-<hash>/fs-<hash>for that pathfs-*(loose by design)systemMessagenaming the call that would satisfy it; advisory by default (exit 0 + stderr), blocking opt-in via plugin setting.PostToolUsematched againstmcp__plugin_lien_lien__(get_files_context|get_dependents|find_similar)writes the sentinels.SessionStartgarbage-collects anygate-sessions/<id>/whose id ≠ the current one.Decisions
~/.lien/indices/<repoId>/gate-sessions/<session_id>/.repoIdderives deterministically viaextractRepoId(cwd)from@liendev/parser(<basename>-<md5(path)[:8]>). Reuses the same path the vectorDB already creates. Will addlien path --store(one-line subcommand) so the shell script doesn't reimplement repoId derivation.systemMessageonly). Blocking is one config flag away. Rationale: a noisy first release will get the plugin uninstalled; advisory still reaches the model viasystemMessage.LIEN_GATE=offenv var (one-shot for the next call) plus a/lien-gate offslash command shipped with the plugin for the duration of the session. No magic comments (fragile).Edit(get_dependentsfor exported identifiers inold_str); tightening offind_similarlooseness for new files.v1 scope (PR-ready)
plugins/claude/hooks/hooks.jsonregisteringPreToolUse(matcherEdit|Write|MultiEdit),PostToolUse(matchermcp__plugin_lien_lien__(get_files_context|get_dependents|find_similar)),SessionStart.plugins/claude/hooks/gate.sh— PreToolUse logic; readstool_input.file_path+session_id; respectsLIEN_GATE=off.plugins/claude/hooks/sentinel.sh— PostToolUse; parsestool_input(and forfind_similar, scanstool_resultJSON for cited filepaths).plugins/claude/hooks/session-clean.sh— SessionStart; removes stalegate-sessions/<id>/.lien path --storeandlien path --extensionsCLI subcommands.Verified against installed-plugin examples
hookskey inplugin.jsonorhooks/hooks.jsonwrapper).${CLAUDE_PLUGIN_ROOT}resolves at runtime.SessionStart/SessionEndare both valid events.PreToolUsepayload containssession_id,cwd,tool_name,tool_input.file_path.PostToolUsematchers handlemcp__*regex (seesecurity-guidanceandhookifyplugins).Simpler alternatives considered
Stophook reminder — fires too late; the edits already shipped.Risks & escape hatches
LIEN_GATE=off.Related