--- name: interlinked-harness description: "Understand and respond to the Interlinked PreToolUse guard — the local daemon that BLOCKS dangerous tool calls before they run. Load this when a Bash command or file edit was refused with \"BLOCKED: … Suggestion: …\", when you see an `[interlinked:check-id]` warning tagged `[proven]` or `[heuristic]`, when a destructive command / force-push / protected-file / secret / repo-confinement rule fired, when a grep was answered by the index, or when you need to know how to legitimately suppress a false positive or disable a guard rule. Covers what blocks, how to read the reason, suppression grammar, determinism tags, and the degraded cold fallback." --- # interlinked-harness — the guard: what blocks you & how to respond ## Predict changes to guards The prediction phase includes a local JS/TS `if`-ownership oracle, independent of Supermodel and `graph_prediction.enabled`. Its default mode is `enforced`; `harness.guard_prediction.mode` in shared `.interlinked/config.json` accepts `enforced`, `shadow` (record/reveal without refusing), or `off`. Before intentionally changing which `if` guards an existing `return` or `throw`, declare the expected ownership change. The raw before/after AST comparison runs before content checks; do not brace or format away an unexpected change. Unpredicted changes block as protocol mismatches, not proven bugs. Correct the proposal, or reconcile the exact revealed change with a rationale and behavioral validation. A matching prediction does not prove correctness. Receipts are JSON at `.interlinked/predictions/guards//.json`. Fields: `version: 1`, `session`, `id`, `file` (canonical repository-relative path), `beforeSha256`, `afterSha256`, `nonce` (nonempty declaration identifier), and `changes`: an array of `{owner, statement, before, after}`. Statement and condition strings use parser token spellings joined by spaces. Each guard is `{condition, branch: "then" | "else"}`; arrays list outer to inner guards. The ID is SHA-256 of `JSON.stringify([1, file, beforeSha256, afterSha256])`, with UTF-8 hashes of the exact source bytes. Use the native session identity printed in a reveal. The reveal includes the measured fields and receipt path. After a reveal, include `reconcile: id` and a nonempty `rationale`; a late prediction alone cannot pass. Identical retries retain their declaration; changed bytes, file or session require a matching receipt. Never use a wildcard acknowledgment. The append-only `predictions/guard-events.jsonl` retains `reveal`, `predicted`, and `reconciled` evidence; acceptance never erases a surprise. Dry runs do not persist events. Invalid or inaccessible protocol state cannot authorize a changed proposal. This is local intent bookkeeping, not an adversarial security boundary. The initial oracle handles unique existing statements in identifiable function scopes, plus unchanged complete function bodies. Changed ambiguous/anonymous scopes and changed statement text produce partial/NOT CHECKED evidence. It does not establish loop/switch/exception dominance, semantic predicate equivalence, deleted statements, cross-file moves, or arbitrary shell effects. Native cold fallback runs the same oracle; a standalone generated hook without the daemon reports NOT CHECKED because it has no AST runtime. Restore the daemon or use `interlinked-hook` for that check. Single-line guards remain valid code; brace style is a separate policy. Claude's demonstrated native `PostToolBatch` boundary coalesces TypeScript checks for declared single-file edits. The final tree is checked before the next model request; security guards and hard complexity caps remain per edit. Final compiler findings are repair context, with unresolved work enforced at Stop/commit. See **interlinked-verify** for fallback and recovery behavior. Merely using multiple Edit calls is not evidence of a retry: correlate their emission time and assistant message with hook feedback. Repeated pre-edit content advice, test-evidence limitations, and unchanged coverage gaps are deduplicated for model delivery. Full observations remain in local evidence. Changing coverage counts alone do not repeat the same instruction; a recovered gap that recurs or a different unavailable-check reason is announced again. Claude and Codex Stop/SubagentStop advisories remain operator diagnostics on stderr. Only explicit blocking repair decisions request another model turn. Source guidance is sent during editing. File-size advice and per-function gates share native edit/patch content projection; unreadable or invalid updates stay unmeasured. Shell effects remain post-execution evidence unless a supported pre-write path is used. PostToolUse classifies recognized untracked Python environments, installed Node dependency trees and named harness outputs before authored-source checks. Explicit edits, tracked files, protected policy and unsafe/symlink paths do not receive this exemption. Security scope stays separate; excluded files are not passes and runtime-input fingerprints are not narrowed. The same source role separates recovery batches. Repeated unavailable-check diagnostics are deduplicated for delivery; raw findings and unresolved evidence remain. Retry when prerequisites change, not merely because the same diagnostic repeats. Missing evidence cannot yield “all clean.” For edits outside the guarded repository, both command-based and named external checks (affected tests and dependency audit) skip this repository's runner. Inline content checks remain available. Security findings on excluded dependency files retain normal severity and enforcement; source-quality exclusion is not a security exemption. The built-in generic `rm` prior-inspection rule is advisory. A recent literal `Read` event does not prove inspection of the deletion targets, and clients may expose different read tools. Inspect the intended targets using the available tools; this heuristic does not require a particular invocation. Deterministic destructive-command, reservation and protected-file decisions still apply. Do not infer safe deletion merely from a recent unrelated read. Guard audit records retain references for repeated warning text. Use `interlinked data search --source warning-occurrences` to inspect full first/changed messages, hashes and occurrence counts; `data show ID` opens their raw evidence. This bounds repetition in the activity mirror without changing the guard decision or the warning returned by the evaluator. A later `not-reported` observation does not prove that the underlying issue was fixed. Interlinked runs a **local daemon** that evaluates each pre-execution event the installed runner exposes to its hook surface (PreToolUse). It is **default-permit with targeted forbid**: it allows everything except known-dangerous shapes, which it refuses with an actionable reason and a safer alternative. It is a fast deterministic guardrail, **not** a security trust boundary (it's local and bypassable) — so the right instinct when blocked is to take the suggested safe path, not to defeat the pattern. ## Load this when For the experimental Cowork plugin, first load **interlinked-cowork**. Its portable guard is a subset of this daemon: exact tool/path denials and the shared destructive command guard. Native crashes/timeouts were observed to fail open; the plugin's launcher mitigates child failure but cannot enforce an absent/provider-killed hook. Host file policies require explicit bridge mapping and matching file snapshots. - A tool call was refused: `BLOCKED: ` / `Suggestion: `. - You see an `[interlinked:]` warning (tagged `[proven]` or `[heuristic]`). - A destructive command, `git push --force`, protected-file, secret, repo-confinement, or bash-redirect-bypass rule fired. - You need to legitimately suppress a check false-positive, or disable a wrong guard rule. ## Mental model - Each installed hook event ships its payload to the daemon over a Unix socket. The daemon runs an ordered set of phases; **the first phase that returns a terminal decision wins**. - Framed daemon RPC validates each method's request shape, including nested hook metadata and administrative arguments, before dispatch. The client accepts only matching-id responses with the expected method result shape. Malformed frames do not become successful decisions; the existing timeout and fallback behavior still applies. - Local quality-check overrides validate each supplied field before merging it. Malformed fields are ignored while valid fields and sibling checks survive; new checks require the complete mandatory configuration. Team overrides affect existing checks only. Default configuration copies preserve non-JSON values such as an unlimited (`Infinity`) step limit. - Content-scanner allowlists skip malformed entries. Known entry kinds require their declared string fields; an unrecognized scanner runtime cannot select a backend. These checks apply to these boundaries and do not imply that every legacy configuration section has a schema. - Decisions: `block` (tool refused, you see the reason), `ask` (human confirmation — Claude and supported Cursor gates can ask natively; interactive Pi calls `ctx.ui.confirm`; headless Pi, OpenCode's stable tool gate, Codex `PreToolUse`, Copilot, and Gemini deny instead; Claude/Codex `PermissionRequest` allow/ask abstain so the provider retains authority), or `allow` (may still carry non-blocking `warnings`, or an `updated_input` rewrite). - Built-in rules are regex patterns on the command/tool-input. Use `interlinked harness checks` for the authoritative current inventory. Patterns are **ORed over positive entries; `negate:true` patterns are exceptions.** `executed_only` masks quoted/heredoc/comment text, so *mentioning* `rm -rf /` in an `echo` is allowed while the bare command blocks. Compound commands (`&&`, `||`, `;`, `|`, newline) are decomposed and each part checked. - Python `open(...)` checks inspect executable calls and recognize write and append modes; read-only calls and quoted examples do not establish a file write. Keep malformed tool input tests at the raw hook-input boundary rather than passing invalid normalized events. - The log-output guard measures ordinary single-file `head`/`tail` windows of up to 200 lines before rejecting a file over 100 KiB. Small windows pass on large logs; a selected line over the byte budget still blocks. Signed counts and unfamiliar option combinations retain the file-size gate. Daemon and generated-hook paths use the same bounded reader. > **Two unrelated things are called "guard".** This skill is the **PreToolUse guard** (the > daemon, operated via `interlinked harness …`). `interlinked guard` is a separate **git > pre-commit/pre-push hook** that checks staged files against file reservations — see > **interlinked-coordination**. ## What gets blocked (with concrete examples) | Category | Blocks (examples) | Allows (examples) | |---|---|---| | **Destructive fs** | `rm -rf /`, `rm -rf *`, `rm node_modules`, `sudo rm …`, `dd`, `mkfs`, `shred`, `chmod 777` | `rm -rf /tmp/x`, `rm -rf dist/`, `rm -rf .wrangler/cache` | | **Process killing** | `pkill node`, `killall wrangler`, `kill -9 1234`, `kill $(pgrep …)`, `… \| xargs kill` | `pkill -f 'wrangler dev'`, `grep -rn "kill -9" src/` | | **Git** | `git push --force`/`-f`, `git reset --hard`, `git clean -fd`, `git checkout -- .`, `git branch -D`, `git stash drop`/`clear`, agent-created worktrees (`git worktree add`; Claude `WorktreeCreate`) | `git push --force-with-lease`; `git worktree list/remove/prune` | | **DB / cloud / IaC / containers** | `DROP DATABASE`/`TRUNCATE`/`DELETE`-without-WHERE, `docker … prune`/`rm -f`, `kubectl delete`, `terraform destroy`, `pulumi destroy` | — | | **Info-flow / persistence** | env exfil (`env \| curl …`, `printenv \| nc …`), `.npmrc`/`.yarnrc` writes, `nohup curl … &`, `crontab -e`, `systemctl enable`, writes to `/etc/cron.d/`, `*.service` | — | | **Protected files** | Read/Write of `*.pem`/`*.key`; Write of `*.env*` **only if secrets detected**; **Delete** of CI configs, `migrations/**`, `.gitignore`, lockfiles, `Dockerfile` | `.env.example` / `.sample` | | **Sensitive-file read** | `Read` of `.env`, `credentials.json`, `service-account*.json`, `*.pem`/`*.key` | `.env.example` | | **Repo confinement** | any Write/Edit whose real (symlink-resolved) target is outside the repo root | paths under the allowlist / session scratchpad | | **Package installs** | any un-allowlisted `npm/pip/cargo/go/…` install; URL/git/tarball specs — see **interlinked-supply-chain** | allowlisted + exact-pinned | | **Bash-routed write bypass** | a `>` / `tee` redirect, `sed -i` / `perl -pi` / `gawk -i inplace` / `ex` / `ed` in-place edit, `patch` / working-tree `git apply`, or a wrapped form (`xargs`, `find -exec`, `timeout`) writing a tracked source file (dodges the content gate) | Write/Edit, `interlinked write`, or index-only `git apply --cached ` | | **Applier-script execution** (`builtin-patch-applier`) | running an interpreter on a pre-existing throwaway script that writes into repo source | route the edit through Write/Edit; committed codegen belongs in `scripts/` | | **Bash-edit obligation** (`bash-edit-obligation`) | a bash-channel edit left an INTRODUCED `pre_block`-class finding on disk; until it is fixed, write-class tool calls to OTHER files are refused (edits to the flagged file and reads stay allowed; the gate re-checks and self-releases) | fix the flagged file first | | **Hand-rolled patch applier** | a throwaway script in the scratchpad or `scratch/` that calls `writeFileSync`/`appendFileSync`/`write_text` on a path outside its sandbox (`"src/…"`, `process.cwd()`, `../`) — a re-implementation of Edit with the gates removed | probes that only READ repo source; scripts writing beside themselves; committed codegen under `scripts/` | | **Content pre_block** (introduced-only) | edit that *introduces* merge-conflict markers, `eval()`, and other zero-FP checks | pre-existing instances (warn, not block) | ## When you're BLOCKED: what to do For selected-hunk staging, prepare and review a patch, then run `git apply --cached `. It changes only the index and preserves other working-tree edits. `git add -p`, `-i`, and `-e` remain blocked as interactive operations; use `git add ` for whole files. Flags on a later shell command do not make staging interactive; a backslash-escaped newline continues the same command and retains its interactive restrictions. A normal `git apply` or `git apply --index` still writes the working tree and must use the content gate. 1. **Read the `reason` and `Suggestion:`.** The suggestion is the intended path (force-push → `--force-with-lease`; `rm node_modules` → `npm cache clean --force && npm install`; `pkill node` → target the PID or `pkill -f 'wrangler dev'`). Take it — but the safe *flag* is not always the whole answer. For a destructive or history-rewriting op on a **shared** target (force-push to `main`, dropping data), the block is also a prompt to check *intent*: confirm the action is warranted and that you have explicit user authorization before proceeding. 2. **Don't rewrite to dodge the regex.** The pattern encodes a real hazard; evading it is the wrong move (and trajectory detectors watch for evasion). 3. **Content-check blocks are introduced-only.** A `pre_block` finding (`[check_id]` in the reason) blocks **only when your content has more instances of that finding's line text than the file already had** (multiset over whitespace-normalized text — moving a pre-existing finding doesn't count; a brand-new file counts everything). Fix the line(s) you *added*, then retry. 4. **Legitimate suppression** — only when a flagged line is genuinely deliberate: - Inline, on the line **above** the finding: `// interlinked-ignore: — ` (reason separator ` — `, ` -- `, or `–`; comma-separate multiple check ids). - File/glob-level: an entry in `.interlinked/verify-suppressions.json` (`{ "": { "": {"reason","by","at"} } }`). - Both are honored identically at pre_block, PostToolUse, and `interlinked verify`, and are **ratcheted/audited** (visible exceptions, not silent bypasses). - Distinct: `// interlinked: defer -- reason` *acknowledges* a finding without suppressing it (still logged) — for pre-existing findings you're choosing not to fix now. - A **`[interlinked:sequence]` pre_block** (trajectory detectors such as `secret_read_then_network_call`) is deferred the same way, but the marker must sit in the call itself — the Bash command text, or the content you are writing — and must name the detector id **exactly**. The call then runs and the harness logs `[interlinked:sequence-deferred]` with your reason. These detectors latch on session state, so without the marker one confidential read can block later outbound network calls. 5. **When NOT to suppress:** `[proven]` findings (tsc/biome/gitleaks/semgrep actually ran) and any destructive/security **guard-rule** block. Suppression directives affect **checks**, not **rules** — you cannot `// interlinked-ignore` a `git push --force` block. If a *rule* is wrong for your repo, the fix is config (below), not a comment. Don't add `@ts-ignore`/`biome-ignore` to silence a `[proven]` check — that trips `suppressions-unjustified`. **Loopback requests after a sensitive read.** The sensitivity guard and `secret_read_then_network_call` share destination-aware classification. Explicit HTTP(S) curl requests to `localhost`, IPv4 loopback (`127.0.0.0/8`), or IPv6 loopback (`::1`) are exempt, including ordinary request/data/header flags. Simple nc/ncat/netcat loopback connections are also exempt. Wget requires `--max-redirect=0` and a supported simple invocation. Each network command and destination must qualify: a local request never exempts another remote request in the same shell command. Sensitivity remains unchanged. LAN/private addresses, unknown destinations/options, shell expansions, curl redirect flags, explicit proxies, alternate resolution and config-file options retain guarding. Explicit environment assignments selecting proxies or client configuration also retain guarding. This is a static command classifier; it does not inspect ambient client configuration or prove what a local service does with received data. Other guards continue to apply. **Commit-risk nudge.** On a real Bash `git commit`, `[interlinked:jit][heuristic]` warns when the currently staged diff ranks at p90 or above against recent commits in this repo. It is advisory, never blocks, and does not replace subsequent commit gates. Inspect it with `interlinked metrics jit --staged --message ''`; see **interlinked-quality-gates** for calibration, interpretation and snapshot limits. An agent must not create a Git worktree to route around shared-workspace policy. Use the current workspace. If isolation is genuinely required, ask a human operator to provision an approved worktree; listing and cleanup of existing worktrees remain allowed. ## Warnings: `[proven]` vs `[heuristic]` ### Filesystem observations and native control limits The daemon watches protected policy files and literal in-workspace reservation paths, then reconciles content identities periodically and at delivery boundaries. Writes by a shell, editor or agent can create durable pending verification entries. Writer identity is `unknown`; a reservation is not proof that its holder performed the write. Glob reservations, symbolic links, inaccessible or oversized files remain explicitly unmeasured. A stopped or unavailable observer cannot certify coverage. `harness coverage status --progress --json` reads cached observation time, generation and recovery counts without reconciling files or returning receipt history. Use it for progress, not to certify current bytes. Plain `harness coverage status --json` still reconciles and returns full evidence. Neither status command starts recovery. Receipts with nonempty `unavailable` retain completed checks as partial evidence while keeping the version pending. Retrying is not made unnecessary merely by having a receipt; safe cross-run reuse needs the relevant check input identities. `harness coverage accept-policy ` records an explicitly reviewed current policy identity. It neither runs checks nor restores old policy bytes. A later Claude ConfigChange can refuse application of a differing identity, after the disk write has happened. This is not an immutable trust anchor or a universal policy reload interlock. Do not describe watcher readiness as baseline-integrity enforcement. Native translations are recorded in `.interlinked/hook-translations.jsonl` by the compiled entry point. A post-tool refusal may become context, continuation or batch cancellation; observational events cannot enforce a denial. Copilot/Gemini approval requests conservatively deny where native approval is not certified. Experimental Antigravity uses `force_ask`; Antigravity and Windsurf refuse a required input rewrite that their hook cannot apply. Read translation status separately from native enforcement. Harness-disable trajectory detection reads executable command positions, so a mutation-test filename containing `kill` is not a process-kill event. Failed commands do not establish a successful disable. A later event served by the daemon retires process/socket-outage suspicion; it does not retire a recorded weakening of `disabled_rules`. Shadow verdicts remain advisory and do not include a misleading `BLOCKED:` prefix. The optional-chain check uses syntax grouping: passing `value?.field` to a function and then accessing that function's result is different from `(value?.field).name`. Without optional TypeScript syntax support it cannot establish this finding; silence is not a clean verdict. SQL migration checks likewise cannot prove a column absent from an interpolated column list. For `it.each(table)(name, callback)`, assertion checks inspect the callback, not functions inside the data table. An unparsed callback cannot prove assertion absence; table assertions also cannot satisfy an assertion-free callback. Every warning is tagged. `[proven]` = a real compiler/linter/scanner/parser/test-runner produced it — authoritative, fix it. `[heuristic]` = regex/AST-shape match that could be a false positive — evaluate it. No tag = unknown check id (never guessed). **Visibility varies by runner.** On the supported Claude and Codex gate surfaces, a block reason is surfaced through the provider's blocking channel. Experimental adapters target their native response or exit-code shapes, but registration is not proof that every provider version enforces them identically. Claude and Codex use model-visible `additionalContext` only on events whose contracts support it. A Claude PermissionRequest deny uses `hookSpecificOutput.decision = { behavior: "deny", message }`, not PreToolUse's `permissionDecision`; allow/ask abstain on stdout, and non-blocking PermissionRequest diagnostics stay on stderr. Cursor uses `additional_context` on generic `postToolUse`. OpenCode appends post-tool feedback to tool output; Pi appends it to `tool_result` and can notify an interactive UI. Copilot remains stderr-only and some lifecycle events are observation-only. Allow-warnings are easy to overlook — read them. Codex `Stop` and `SubagentStop` request continuation only for an explicit harness `block`. Its reason is preserved before any additional context or warnings. An `allow` or `ask` decision emits no stdout; diagnostics stay on exit-0 stderr for operator inspection, not model-visible continuation. These events have no native approval channel. Pending hook-coverage evidence alone does not force another turn or become checked by allowing completion; inspect coverage status separately. **Silence = no model-visible finding, NOT "everything was checked" (2026-08-27).** A served, clean PostToolUse result writes ZERO BYTES — no `[interlinked:Bash] all clean (354ms)` row, not even an empty `{}` envelope — because runners render one hook row per response and parallel tool calls multiplied that into unusable noise. Outage visibility is phase- and runtime-specific: | Result | What you see | |---|---| | Clean PostToolUse result served by the daemon | nothing (recorded locally only) | | Findings | one compact `[interlinked:]` block | | Block | the reason plus the affected target — **including a block that carries no warnings** | | Daemon unavailable, packaged PreToolUse runtime | an explicit `evaluator skipped` diagnostic; code edits also report function-token enforcement as not measured | | Daemon unavailable, generated PostToolUse runtime | may remain model-silent while recording local `no_harness` status | | `typescript` unresolvable (`--omit=optional` install) | a `[interlinked:self_import] NOT MEASURED` warning on every JS/TS edit — that pre_block check ran no scan and never guesses; the cyclomatic gate degrades to the regex walker; the daemon startup warning names both | | `self_import` cannot place the file in ONE project | the same `NOT MEASURED` warning on that edit only, naming the cause: a tsconfig on the walk (nearest config, its `extends`, its `references`, its sibling `tsconfig*.json` files) could not be parsed; two projects both claim the file; the reference walk hit its 32-project bound with projects unvisited; no config in reach claims the file by a `files`/`include` pattern (a `.js`/`.jsx`/`.mjs`/`.cjs` importer is matched by pattern even when the project never enables `allowJs` — a JS self-import is a runtime fact); or the importer's directory is not on disk yet. The check resolves with the project's own compiler options and caches nothing, so fixing the config applies on the next edit. In a batch (`write --batch`, `multi-edit`, `verify-changeset`) it sees the whole proposed batch, and the baseline is judged against the disk | | a batch rewrites a file of the project's configuration graph — the selected tsconfig, any file its `extends` chain reaches (whatever it is called, `base.json` included), or any `tsconfig*.json` / `jsconfig.json` / `package.json` | each TypeScript entry of that batch carries a `type checker cannot see the proposed configuration` row (error for transactional callers) — the checker reads the disk's config; land the configuration change first, then the sources | | a batch includes a member whose bytes equal the disk beside members that differ | the unchanged member is still type-checked, against the proposed siblings (a changed exporter can break an untouched consumer); `multi-edit` validates every manifest member and writes only the changed ones. Only a batch whose members are ALL unchanged skips the check. A config member whose bytes equal the disk is not a configuration rewrite | | the type checker cannot place the file in ONE project | `type checker unavailable (project_orphan: …)` or `(project_ambiguous: …)` on that edit — a warning on the hook path, an error for transactional callers. The compiler judges each file under the SAME project `self_import` selects (nearest config, `references`, sibling `tsconfig*.json`, membership by pattern; one program per governing config) and never under the project root's config by guess. Its disk baseline is recomputed on every check, so a dependency repaired or broken on disk is reflected the next time the file is judged. A warm compiler service is rebuilt when the CONTENT of any file of its configuration graph (the tsconfig or an `extends` target) changes on disk, re-reads the project's root files on every reuse (a declaration file added or removed on disk joins or leaves the program), sees a dependency rewritten on disk by its compiler TEXT (every UTF-16 code unit is preserved, including lone surrogates; a touch that changes no text does not count; each check reads a file once, so what it compiles is what it fingerprinted), forgets a proposal the moment its check ends (a refused proposal never colours a later check), and a configured project with no source on disk yet still measures the first source written into it | No output therefore never proves the full daemon check set ran. To distinguish a served clean result from a silent degraded PostToolUse path, ask `interlinked harness status` or `interlinked doctor`; inspect `.interlinked/activity.jsonl` and the statusline for the per-call state. The block row is load-bearing: the response path decides on the DECISION, never on whether the warning list happens to be non-empty. If you are about to rely on "the harness would have caught it", confirm the daemon is answering first — silence is not evidence that it was. **Test edits use the same hook loop.** Deterministic introduced test theatre (assertion-free or tautological cases, SUT self-mocking, focus markers, unconditional skips) blocks before the write. Lower-confidence but low-noise test-quality findings are PreToolUse warnings so even a small writer can correct them immediately; context-heavier suite review stays in PostToolUse. Do not silence a warning merely to land the edit—assert a precise observable behavior, or justify the actual public compatibility contract. With explicit `structural_checks.test_first_mode: "enforce"`, an empty repository does not exempt new JS/TS or Python source from companion-test requirements. Write the first meaningful test, observe its failure, then implement the behavior. Existing pair-debt workflow remains available after a suite exists; warning mode stays advisory. This creation gate does not itself prove that every later change followed red–green TDD. **Test-discrimination advisories** are `post` warnings and also run under `verify --all-checks`. They review fallback-only evidence, indistinguishable positive/negative expectations, unpinned spy arguments, wildcard observables, in-tree fixtures, fixed ports, ambiguous throw messages, unguarded catch assertions, vacuous loops, mock return echoes, duplicate bodies, spies without restoration, export existence smoke tests, and commented-out assertions. They never block. Evaluate the intended contract: forwarding a mock value or preserving an export can be legitimate. Fix a detector false positive before rewriting valid tests. `spy_without_restore` recognizes applicable local cleanup and statically discovered Vitest/Jest restoration settings; `clearAllMocks`/`resetAllMocks` are insufficient. Fresh test-owned array literals are exempt, as are fresh test-owned objects; module/suite arrays, shared aliases, and prototype methods still need restoration. Source headers and `docs/design/test-discrimination-checks.md` describe limits and calibration. A corpus hit count is not precision or proof of a surviving mutant. ## Bash effects and sandbox evidence Do not trust a tool name as proof that no file changed. For Bash and other potentially mutating tools, the daemon snapshots Git-visible files plus standalone ignored local files (for example `.env`, while collapsing bulk ignored directory trees) before the call and attaches the observed created/modified/deleted ChangeSet after it. PostToolUse file checks prefer those observed paths over command text or runner-declared paths. **A read-only tool contributes no observed paths.** The ChangeSet is a diff of the window the call occupied, not a record of what the call did, so on `Read` / `Glob` / `Grep` / `WebFetch` / `WebSearch` / `TodoRead` / `NotebookRead` / `ListFiles` every path in it was written by somebody else — another agent on the same tree, a background test run, a watcher. Those calls are charged nothing and run no file checks. `Bash` is NOT in that set and keeps its ChangeSet (it is the bash-edit obligation channel), and neither is an unknown tool, so a new writer cannot open the bypass by using an unfamiliar name. The list is one definition, at `src/lib/hook-read-only-tools.ts`. Claude Code's PostToolUse hook is also registered only for `Write|Edit|MultiEdit|NotebookEdit|Bash`; Codex keeps the all-tools matcher because `apply_patch` arrives through it. Reservation handling parses every `apply_patch` section destination and move source once; PreToolUse grants and PostToolUse idle-release scheduling consume the same ordered, de-duplicated path list. It preflights that full list before granting, so a later blocking path cannot strand an earlier lease when the tool never runs and therefore has no PostToolUse. A Stop-time `[interlinked:effect-residue]` warning means a PostToolUse was missing/unreconciled; the observed files were added to the touched-file rescan. Effects reconciled by another actor are excluded (the warning reports the excluded count) — this includes another session and a sibling subagent that shares the same Codex session id, so one parallel actor is not charged for another's work. Actor identity is evidence-bounded: Interlinked uses the stable subagent id when Codex supplies one and otherwise stays at root-session scope. Persisted pre-upgrade rows with no actor field remain conservatively session-scoped; new rows distinguish a known root from a known child across daemon restarts. The noisy `.interlinked/` runtime tree stays collapsed, but its exact local policy/control files are observed and cannot be silenced by `skip_paths`. Interpret `[interlinked:sandbox]` as evidence visible to the hook: - `attested` — the runner marked this call sandboxed; - `configured` — restrictive client config was found, but a CLI/profile override may differ; - `disabled` — the call/config explicitly selected unsandboxed/escalated execution; or - `unknown` — no trustworthy evidence reached the hook. A workspace-write sandbox limits blast radius but still writes the real project, so it is defense-in-depth, not rollback. Do not rerun or rewrite a command to evade this warning. For changes that require rejection before disk, use Edit/Write or gated `interlinked write`/`multi-edit`. The transactional commands share the content gate and commit lock. They capture target state before checking, abort on target drift, preserve modes, and refuse rollback over newer edits. Unavailable Biome or TypeScript checks abort a transaction. Re-read and re-gate a conflicted proposal; consult **interlinked-verify** for the command contracts and transaction limits. The observer is bounded ordinary-process evidence: concurrent writers can cause conservative extra attribution, and an incomplete snapshot is never proof of absence. ## Compiler acceleration The separate Bash compiler accelerator recognizes `tsc` / `npx tsc` only in executable positions of flat, quote-aware command lists. Quoted search patterns and look-alike names are never compiler invocations. Commands with comments, newlines, substitutions, or grouping run unchanged through the normal tool. ## Grep acceleration The guard intercepts `Grep` tool calls and Bash `rg`/`grep`, queries a trigram index for candidate files, and can answer the search directly (block-and-answer) faster than a full scan. It is **strictly never-worse-than-native**: on small/medium repos it declines and native `rg` runs unaccelerated. It's a large-monorepo optimization; you don't manage it. Inspect what it would match with `interlinked index query `. Your own just-written edits are immediately searchable (in-memory dirty layer). ## Configuring the guard (when a rule is genuinely wrong for the repo) Config lives in `.interlinked/guard-rules.json` (team) + `.interlinked/guard-rules.local.json` (personal, gitignored). Merge priority: local > team > built-in. - `disabled_rules: ["builtin-git-force-push", …]` — turn a built-in rule off (built-ins can be disabled, never edited). - `extra_exceptions: { "": ["substring", …] }` — the rule won't fire if the command contains a listed substring. - Team/local/distilled files hot-reload within ~2s (`watchFile`); `interlinked harness restart` forces a full reload. ## The cold fallback (daemon outage degrades; deterministic gates stay closed) Hook invocations record their transport outcome in `.interlinked/hook-transport.jsonl`: `daemon` means a valid daemon response, `cold` means fallback, and `suppressed` means intentional repeated-Stop suppression. Correlate `event_id`, `session_id`, `native_event`, `hook_pid`, `socket_path` and `protocol`. The hook PID identifies the receipt writer; verify daemon PID ownership separately. Daemon activity rows carry `writer: "daemon"`; the generated hook also writes activity, so an unmarked activity row is not proof of daemon handling. If the configured daemon socket is unreachable — including a **zombie** with a live process but dead listener — every ordinary hook phase enters a cross-process, single-flight recovery path. Daemon absence alone does **not** blanket-block safe reads, diagnostics, or repair work. The current call proceeds in degraded mode after the inline deterministic subset runs: merge-conflict markers, **destructive commands**, **package installs**, graph-shard protection, and file-dump limits. Native fallback also enforces local guard-prediction reconciliation; physical file size only advises. Checks needing the full evaluator are explicitly unavailable rather than silently reported clean. `interlinked harness status`, `harness start` / `restart`, `doctor`, `disable`, and the exact `interlinked install-hooks --refresh --preserve-mode` repair remain executable during an outage and do not race the automatic launch. A recovery message says “launch attempted” only when this hook actually spawned; lock/backoff/stand-down paths do not claim that a supervisor is bringing anything back. A valid `guard-disabled.json` or `guard-disabled.local.json` marker suppresses self-heal. `interlinked harness status` confirms recovery — a red `ZOMBIE` line means a live PID is answering nothing, so trust the socket probe over the PID. See **interlinked-setup** for the liveness states and startup-failure ledger. If the installed hook binary itself is missing or broken, the self-contained wrapper follows the same no-deadlock boundary: Claude/Codex reserved read builtins and Cursor's dedicated `beforeReadFile` event proceed with an explicit degraded warning. Ambiguous generic names on other gates do not. Exact build/status/repair/non-destructive-disable commands remain available, while mutating and unknown tools receive the provider's native deny response. Shell commands are never inferred read-only from their text. Restore the runtime before relying on the full evaluator. ## Gotchas - **The "`sleep` is blocked" claim in old docs is stale** — there is no standalone `sleep` block in the current rule set. Don't rely on it. - **`interlinked harness test ""`** fires a synthetic event at the daemon to see if it would block — and is itself exempt from destructive rules so you can test them safely. A chained destructive tail still blocks; "test allowed it" ≠ "the real command is allowed". - **`harness restart` clears per-session trajectory state** — soft_block "retry allowed" memory and trajectory detectors reset. A restart mid-task can change guard behavior. - **SessionEnd heavy checks are single-flight per daemon** — a burst of agent shutdowns does not start duplicate recurrence scans or coverage ratchets. While one detached job is still active, later SessionEnd events log it as already running and skip that copy; the next run becomes eligible when the child exits or the daemon restarts. - **Shadow trajectory observations** are recorded in `check-executions.jsonl` as `hook-observation.v1` rows (`kind: trajectory`), without model warnings. They preview possible rules and do not enforce the reported action. Enforced trajectory rules remain active. - **PreToolUse blocking**: Claude Code and Codex are supported. Claude also registers PermissionRequest; Codex registers all twelve native lifecycle/tool events. Codex PreToolUse `ask` becomes deny, while both providers' PermissionRequest `ask` preserves the native user prompt. Codex `Interrupt` is asynchronous observation only: it emits zero stdout and never runs Stop/SessionEnd cleanup. Cursor, Copilot, and Gemini adapters can register and parse events, but their end-to-end provider enforcement remains experimental (no provider-level contract test). Copilot/Gemini collapse `ask` → deny. OpenCode and Pi are experimental managed bridges: OpenCode hard-gates generic tool execution but `ask` denies and its permission/Stop signals cannot control the provider; Pi gates both `tool_call` and direct `user_bash`, asks through an interactive UI, and denies headless. Neither has dedicated native MCP, subagent, or worktree lifecycle events. Do not infer an absent event from silence; the shared `git worktree add` shell block still applies. - **PII content scanner** is separate and opt-in: `interlinked scanner on|off|toggle|status|review`. - Env bypasses (logged, documented-flows only): `INTERLINKED_DISABLE_PACKAGE_GUARD=1`, `INTERLINKED_DISABLE_BASELINE_GUARD=1`, `INTERLINKED_DISABLE_SCRATCH_GUARD=1`. ## Quick reference ```bash interlinked harness test "git push --force" # would this block? (safe to run) interlinked harness checks # authoritative check inventory interlinked harness status --json # liveness + socket_answered interlinked index query "" # what the grep accelerator would match interlinked harness restart # reload everything (clears trajectory) ``` ## Related skills - **interlinked-verify** — the check catalog behind the warnings, `interlinked verify`, and how to land edits through the gates. - **interlinked-quality-gates** — advisory file size and the coverage/complexity/function-token ratchets. - **interlinked-supply-chain** — the package-install gate in detail. - **interlinked-setup** — starting/restarting the daemon, `doctor`, config.