--- name: interlinked-setup description: "Install, operate, and troubleshoot the Interlinked CLI harness in a repo — install/uninstall agent hooks, connect runners (Claude Code, Codex, Copilot CLI, Gemini, Cursor, OpenCode, Pi), start/stop/restart the local guard daemon, run `interlinked doctor`, switch server / sync-mode / check-policy, log in, and manage the two-tier `.interlinked/` config. Load when setting up Interlinked, when `interlinked doctor` reports problems, when the guard daemon is down or stale, when hooks are not firing, or when configuring or disabling Interlinked." --- # interlinked-setup — install, operate & troubleshoot the harness The local guard-prediction oracle defaults to `enforced`, independently of the optional Supermodel graph protocol. Shared `.interlinked/config.json` accepts `harness.guard_prediction.mode: "enforced" | "shadow" | "off"`. Use **interlinked-harness** for exact prediction/reconciliation receipts and limitations. Native cold fallback can run this AST oracle; standalone generated hooks need the daemon and report NOT CHECKED without it. Physical file size is advisory in both warm and cold paths; rebuilding must not revive the old line-count block. Claude compiler batching activates only after a session sends native `PostToolBatch`. After daemon restart, edits use the ordinary compiler path until that boundary is observed again; previously queued work remains durable and is checked at Stop/commit. Keep the installed PostToolBatch hook current. It delivers repair context rather than cancelling the agent loop for ordinary compiler findings. See **interlinked-verify**. Skill refresh preserves modified installed copies. If they still describe sub-cap complexity slew blocks, compare them with canonical `skills/*/SKILL.md`, retain local customizations/backups, and reconcile deliberately. Restarting a daemon cannot erase guidance already loaded into a model's conversation. Non-gating native hook commands use a compact missing-runtime diagnostic and exit 1; gating hooks retain the self-contained fail-closed fallback and exact repair allowlist. After rebuilding, use `interlinked install-hooks --refresh --preserve-mode` to refresh installed command text, then verify `interlinked harness status --json` shows the serving build. Keep PostToolUse scoped to mutating tools; do not add PostToolUseFailure duplicates. Provider qualification must exercise native edit routes, advisory file-size delivery, and completion after ordinary Stop advisories. Claude and Codex keep those advisories on stderr; explicit repair blocks still request continuation. Codex rewrites include `permissionDecision: allow`. Retain registrations and native transcripts: an encoded response alone does not prove native enforcement. Before behavioral qualification, use `interlinked tests readiness --json`, then run a real project suite. Python readiness probes the selected environment for pytest, pytest-cov and coverage; it does not install or infer passing tests. The hook installer also ignores the named generated payload-key, enforcement-status and coverage-runtime-estimate outputs. These ignore entries never exempt policy files or make runtime-input caching sound. For an externally frozen policy, keep the policy files on read-only supervisor mounts and pin their parent directories as mount points; a read-only file under a replaceable parent is insufficient. Keep daemon sockets, atomic receipt writes, proposals and intended ratchet state writable. Qualify CLI mutations, direct replacement, local overrides and daemon restart under the actual non-root agent identity before claiming immutable effective policy. A same-UID daemon and `--by` labels do not provide an authorization boundary. Interlinked is a **local, offline-first guard layer** for AI coding agents. Repository config, logs, and daemon state live under `/.interlinked/`; installation also writes selected providers' settings, managed bridge files, and installed skill directories at their provider-specific project or user locations. A **local daemon** ("the harness", a Unix-socket server) evaluates each event the installed runner exposes to its hook surface. This skill covers turning it on, keeping the daemon healthy, configuring it, and turning it off. The remote server is **optional** — hooks, guard, and activity capture work with zero network. `interlinked enable` adds named daemon telemetry, capture state and test-run outputs to `.gitignore`. Re-running setup updates older installations whose generated ignore list is incomplete; otherwise harness bookkeeping can appear as new workspace edits and schedule unnecessary checks. This does not ignore the entire `.interlinked` tree or change runtime-input validation: shared policy/ratchet files remain observable, and deliberately tracked output still participates in Git-visible change capture. Do not untrack files automatically. ## Load this when - Installing Interlinked or connecting a runner (Claude Code / Codex / Copilot CLI / Gemini / Cursor / OpenCode / Pi). - `interlinked doctor` reports failures, or hooks are not firing / not capturing events. - The guard daemon is down, stale, a **zombie**, or hooks report degraded inline fallback. - Changing the server URL, sync mode, or check-policy/operational tier. - Disabling or fully removing Interlinked. ## Mental model - **Per-`cwd`, offline-first.** Repository config and runtime state are rooted at `/.interlinked/`: `config.json` is committed/shared and `config.local.json` is gitignored/personal. Provider hook settings, managed OpenCode/Pi bridges, statusline entries, and installed skill copies live in the provider's project or user directories; the installer manifest records the integration files it owns. - **A daemon does the work.** `harness start` runs a background Unix-socket server. When it is up you get the full check set; when it is down the hook falls back to a small inline subset and **fails closed on the dangerous stuff** (destructive commands, agent-created worktrees, package installs, line-cap, merge conflicts). - **The server is optional.** Auth / sync only matter for server-backed coordination (see `interlinked-coordination`). Skip it entirely for local-only use. - Classifier API-key lookup accepts nonempty strings from the configured environment variable first, then the exact or lowercase key in `config.local.json`. Malformed JSON roots and non-string local values do not become credentials. - **Semantic search is optional and local.** Setup never downloads model weights. The explicit `interlinked semantic install` command is the only model-acquisition path; see **interlinked-semantic-index**. ## Turning it on Cowork has a separate plugin workflow: `interlinked cowork package --output `. Load **interlinked-cowork** for upload, native conformance, runtime failures and the optional authenticated host bridge. It is not an `enable --clients` coding-client target; editing Claude Code settings does not install the Cowork plugin. `interlinked doctor` also inventories existing lint configuration and offers `interlinked lint import`. Preview is read-only; `lint import --write` enables the registered PostToolUse wrapper, and adding `--baseline` executes installed linters to adopt existing debt. A local `lint_import.enabled:false` override is reported before import writes. Hooks must already be enabled. Follow **interlinked-verify** for supported adapters, unresolved configurations, and the explicit `lint check` gate; the ordinary `adopt` command remains separate. Oxlint and named ESLint configs are detected automatically; `lint import` previews their scopes and hook/audit cadence. For arbitrary filenames, use repeatable `--config tool=file` (or `--eslint-config file`), then `--write --baseline`. Selectors are relative to the target project; `--eslint-scope` optionally chooses a package working scope. Saved profiles survive re-import without repeating these flags. Script aliases, task/CI evidence and local config/plugin imports extend discovery. Use `.interlinked/lint-adapters.json` for reviewed direct SARIF analyzers; follow **interlinked-verify** for that contract. YAML task parsing requires the optional `yaml` package; without it the evidence remains in the review list. Dependency approval follows **interlinked-supply-chain**. Slow/type/CI profiles initially use audit cadence; `lint check` or `verify --all-checks` includes them. | Command | What it does | When to use | |---|---|---| | bare `interlinked` (unconfigured repo) | **The harness-first setup wizard** (2026-08-16): six one-line decisions — runners to hook, enforcement mode (`strict` — the recommended default — /`lenient`/`balanced`), review scope (`diff`/`whole-file` → `guard-rules.json` `diff_aware.enabled`), cap overrides, brownfield `adopt`, and dead-code posture (2026-08-17: `flag` default / `delete` instructs the agent to remove flagged dead code in the same edit / `off`; writes `structural_checks` scoped to only the dead-code checks so the rest of the family stays a separate decision; the whole-repo sweep is `interlinked deadcode`) — each Enter-accepts a recommended default, shows the plan, then composes `enable` + `mode` + `caps set` + `adopt`. Local-first: never asks about a server. Non-TTY: env-driven (`INTERLINKED_MODE` / `INTERLINKED_SCOPE` / `INTERLINKED_ADOPT` / `INTERLINKED_CLIENTS`). A failed step reports and continues — every step is individually re-runnable via the owning command. Ends with a **posture receipt** (2026-08-17): one line per thing now enforced, each naming the command that changes it. | A new user's first touch; the fastest correct install. | | `interlinked enable` | Full setup: writes `.interlinked/` config, installs per-client hooks, updates `.gitignore`, installs statusline (Claude/Copilot), installs every bundled Interlinked skill, **auto-starts the daemon**. Idempotent; re-running **clears any stand-down**. | The normal way to turn Interlinked on. | | `interlinked setup` | Runs `enable`, then `login` if no token (skips login on localhost / when a token is present). | You also want server auth right away. | | `interlinked init` | Interactive/auto onboarding wizard (`--yes` for non-interactive): installs hooks on its own path, logs in, attaches a workspace — and installs **no skills**. | Guided team/workspace setup only. | | `interlinked install-hooks` | Adapter path: writes hook entries + `installer-manifest.json` and, unless `--preserve-mode`, applies the selected/default enforcement mode. `--refresh` is manifest-scoped hooks-only and implies preserve-mode; installs **no skills**. | Precise, manifest-tracked hook install or refresh. | **A mode is a posture, not just check severities (2026-08-17).** `interlinked mode strict|balanced|lenient` also ladders the philosophy-dependent gates into `guard-rules.json` (merge-preserving; later hand edits win until the mode is re-applied): the new-file TDD gate (`strict`=block, `balanced`=warn, `lenient`=off), per-edit coverage (`strict`=no-debt blocking, `balanced`=debt mode, `lenient`=off), and the session-end verification/commit-cadence nudges (`lenient` turns them off). Security rails and tighten-only ratchets never ladder. `custom` applies nothing. **Trigram indexing is opt-in:** `enable` installs hooks and starts the daemon without building the repository's search index. Run `interlinked index build` to enable grep acceleration; an existing index continues to load normally. Keeping the repository scan out of onboarding allows setup to finish on large repositories without exhausting the CLI's heap. `adopt` still builds the index and snapshots existing manifests/lockfiles into the install allowlist (`approved_by: "adopt"`). > Bare **`interlinked` is the recommended human first run** because it shows the local posture > decisions before composing them. `enable` is the canonical install primitive for automation > or an already-chosen configuration. Prefer either over `install-hooks`: only the wizard and > `enable` install the skills that teach an agent how to read a block. Without them the first > block is a message the agent must guess at, and the likeliest guess is to work around the gate. Key `enable` flags: `--server ` · `--agent ` · `--clients ` (`claude,copilot,gemini,codex,cursor,opencode,pi`) · `--sync-mode ` · `--data-dir ` · `--structure ` · `--dry-run`. `install-hooks` uses different vocabulary: `--runner ` · `--scope ` · `--mode `. Explicit values are strict: an unknown client, sync mode, structure mode, install scope, or enforcement mode exits nonzero before config, hook, manifest, stand-down, binary-fallback, daemon, or login writes. Only an omitted option receives its documented default. ```bash interlinked enable --agent my-bot # detects clients, starts daemon interlinked enable --clients claude --dry-run # preview without writing interlinked install-hooks --runner claude-code --scope project ``` > Client detection uses the registry's project directory/config markers and process environment, > not a PATH probe for the binary. Codex automatically detects skill changes; if an active session > does not, restart it. Copilot needs `/skills reload` to pick up skill changes. OpenCode/Pi native > skill copies live under `.opencode/skills/` and `.pi/skills/`. ## Native shell sandbox posture Enable each client's strictest usable native sandbox separately; Interlinked hook installation does not turn the provider sandbox on. Bash PreToolUse reports `[interlinked:sandbox]` as `attested`, `configured`, `disabled`, or `unknown`. Treat `configured` as weaker than per-call attestation because CLI/profile overrides can change the active call. Explicit Codex escalated/`danger-full-access` calls and Claude unsandboxed settings are reported as disabled evidence. Native workspace-write sandboxes constrain host/network reach but still allow real writes inside the project. They complement Interlinked's post-call filesystem ChangeSet and Stop residue backstop; they do not provide rollback or replace the deterministic PreToolUse guards. See **interlinked-harness** for interpreting the warning. ## Operating the daemon `interlinked harness capabilities --json` reports runtime hook profiles, installation and emission evidence, and filesystem coverage. `harness coverage status --json` shows pending identities, manual reviews, automated check receipts, and any recovery job. Coverage control calls allow ten seconds per transport for reconciliation and retained receipts. A timeout still means no verdict; inspect status before retrying a mutation, since the daemon may have completed it before the response was lost. For `[interlinked:hook-coverage] NOT CHECKED`, run `harness coverage verify --json` and follow **interlinked-verify** for evidence scope, deferred checks and reviewed absences. If a running daemon predates the verifier, rebuild/restart using the normal lifecycle. Observing or checking protected files does not accept their policy digest. Local data maintenance has its own `data.config.json` in the resolved data directory. `interlinked data configure --auto-index on` enables bounded SessionEnd index jobs; `--auto-compact on` also enables lossless collection/timeline rotation. Both default off. These settings are reread per maintenance job and do not alter capture, sync, or guard policy. Use `data health`/`data status` for producer coverage and index freshness; load **interlinked-observability** for budgets, search, archive behavior and audit checkpoints. | Command | Purpose | |---|---| | `interlinked harness start [--verbose] [--json]` | Start the daemon (background). Reaps orphans first and auto-rebuilds stale `dist/`, so a cold start can take a few seconds. Freshness recursively checks non-test product files, including edits to existing nested files. Readiness requires live connections to every required listener; a socket inode alone is not ready. | | `interlinked harness stop` | Send SIGTERM, wait up to 3 seconds, then SIGKILL only verified surviving daemon targets; report any process that truly survives. | | `interlinked harness restart` | Stop + fresh start; **the only way to pick up config/mode changes**. Note: clears per-session trajectory state. Defers instead of killing when a start is already in flight, and backs off with "Too many restart attempts" if too many restarts (any trigger) went unresolved recently — see below. | | `interlinked harness status [--json]` | **Liveness** (three states, below) + socket, RSS, mode, orphan count, build staleness. `--json` adds `liveness` (is anything SERVING — raw StatusQuery or a framed `daemon.health`), `socket_answered`, and `pid_running` (alias of `running`; BOTH mean only "the legacy `harness.pid` names a live process" — a framed-only daemon can be `liveness: "listening"` while `running`/`pid_running` are false). Build staleness ignores test files (`*.test.*`, `__tests__/`) — they are not bundled into the daemon. | | `interlinked daemons [--cleanup]` | List **all** per-session daemons; `--cleanup` purges dead-PID records. | | `interlinked harness reap [--force] [--all]` | List (default) or kill orphan daemons. | ```bash interlinked harness status --json # is it up? which mode? interlinked harness restart # after editing config / changing mode ``` `harness restart` performs that recursive freshness check and any required build **before** stopping the serving daemon, in normal and `--json` modes. A failed build, or one that leaves `dist/` stale, aborts the restart and keeps the incumbent serving; Interlinked never knowingly replaces it with stale code. ### Liveness is a round-trip, not a PID `status` and `doctor` both send a real event and wait for the answer, because a daemon can stay process-alive while its listener is dead. Three states, and both surfaces use the same words: | `harness status` | `doctor` | Meaning | |---|---|---| | `running (PID …) — socket answering` | `pass` — `Running (PID …) -- socket answering` | Verified: something answered. | | `ZOMBIE — process alive (PID …), no socket answering` | **`fail`** — same remedy line | Full evaluation is unavailable; deterministic inline gates remain active and ordinary work proceeds degraded. | | `not running` | `warn` — inline fallback | Honest and expected; the inline subset still guards. | Only an answered probe prints `running (PID …)`, so that line can no longer appear above `Socket: not found`. **Fix a zombie with `interlinked harness restart`** — both surfaces print that remedy inline. A pid-alive daemon gets one confirming re-probe, so a daemon still binding right after `restart` is not mislabelled. Every ordinary hook event may trigger bounded recovery; SessionStart is an opportunity, not a prerequisite. The startup mutex collapses concurrent hooks to one launch. Startup and restart wait for live connections rather than socket-file existence, and the startup lease is released immediately after the final required listener binds or startup reaches a terminal failure. A daemon quiescing for shutdown keeps the mutex under its own PID until it exits so a successor cannot overlap its live heap. A dead owner is reclaimable immediately. A valid team or local guard-disable marker suppresses auto-start; a malformed marker fails toward guarding. During the gap, safe reads, diagnostics, and repairs proceed through the inline fallback while deterministic dangerous operations can still be refused. Status/start/restart/doctor/disable and exact preserve-mode hook refresh commands are excluded from auto-recovery so the hook never races the operator. ### Restart defers to an in-flight start, and backs off under churn (2026-08-22) `interlinked harness restart` used to stop-then-start unconditionally, so two overlapping restart triggers (a build-refresh handover, an rss-ceiling recycle, a second manual restart) could kill a successor the other had just spawned, before it finished binding. Now: - If a start is already in flight (the daemon is mid-boot), `restart` waits for its socket instead of killing it — printing "already in flight" and doing nothing further once it answers. - If too many restart attempts (any trigger) went unresolved in the last ~10 minutes, `restart` refuses and prints "Too many restart attempts … backing off" instead of adding to the churn. Check `.interlinked/daemon-events.jsonl` for the pattern before retrying by hand. Neither path is silent: every deferral or backoff writes a `handover` row to the daemon ledger (`daemon-ledger.ts` / `handover-churn.ts`), so `daemon-events.jsonl` always explains what `restart` actually did. Since 2026-08-29 every handover attempt is one ID across the whole chain: the parent daemon writes `requested` → `launcher_spawned` (the restart CLI was launched), the restart CLI adopts the id and writes its own `requested` intent, the actual daemon spawn writes the COUNTING `daemon_spawned` row, and the successor's `listening` row acknowledges that exact id — order-independent. Terminal rows (`refused` on backoff/deferral, `spawn_failed`, `no_artifact`, `start_failed`, and a `startup-failed` exit stamped with the id) resolve the attempt the same way. Only an unresolved `daemon_spawned` counts toward the backoff; `explicit-restart` / `explicit-stop` / deferral rows are audit facts and never count. A manual `interlinked harness start` (id-less `listening`) still pays off one preceding unresolved attempt. If the backoff refuses `restart` during a genuine outage, `interlinked harness stop` + `interlinked harness start` is the ungated recovery path. ### Over the memory ceiling, the daemon stops before replacement (2026-08-31) Interlinked CLI's proposed design baseline is an 8 GB whole host, shared with the editor, browser and coding agents. The operator plan is `docs/plans/8gb-host-resource-plan.md` (private operator material, absent from public clones). Its aggregate budgets and qualification criteria are not shipped defaults or a claim of 8 GB readiness. Keep the current limits below until retention improvements and constrained workflow measurements justify replacing them; lowering heap flags alone can cause restart loops. SessionEnd jobs now run through a detached supervisor with a 128 MiB V8 heap cap. The supervisor holds a per-project/job lease through child cleanup, including when the daemon exits, and admits only one background runner per host at a time. Scheduled foreground Vitest runs and verify share this host slot. A waiting foreground request prevents another background admission; the supervisor's monitor aborts the active background child group so foreground work can proceed. The daemon's async project-test and legacy affected-test processes share this slot too; outer project admission does not exempt them from host admission. Their Node heap is bounded at 768 MiB and `VITEST_MAX_WORKERS=1` bounds compatible Vitest runners. Independent `npm test` processes remain outside this admission system. A duplicate background job is skipped; a different job waits at most two minutes. Each admitted runner has a ten-minute deadline and a 768 MiB Node V8 heap cap. Fuzz and benchmark commands receive an explicit `--maxWorkers` value, rechecked after waiting for admission. Admission respects physical RAM and any process/container limit. On an unconstrained Mac, available memory is estimated from free plus inactive pages (`vm_stat`), with native `kern.memorystatus_vm_pressure_level` required to be normal. Node's free-page reading alone does not account for reclaimable cache. Warning/critical pressure or unavailable telemetry defers work. Samples are cached for at most 500 ms and commands have bounded time/output. Managed test and background runners budget at most 4 GiB or one quarter of total memory, whichever is smaller, and preserve at least 1 GiB or one eighth of total memory as host headroom. Worker planning budgets 1 GiB for coordination plus 1 GiB per worker. Thus an otherwise idle 8 GiB machine gets at most one worker and a 16 GiB machine at most three. Missing or insufficient available-memory readings defer work. A 500 ms monitor measures runner and descendant RSS with bounded, non-overlapping `ps` samples. It aborts the child process group when the runner exceeds its budget, headroom disappears, or telemetry fails. Scheduled foreground tests use this monitor too, and their coordinator receives a 1536 MiB V8 heap ceiling. Interruption retains the pending request and produces no reusable pass. Timeout and termination reap the child process group before releasing admission. This is sampled protection, not an OS-enforced RSS limit: allocations can overshoot between samples, descendants that escape the process group are not contained, and unsupported `ps` telemetry interrupts the run. Public `verify` and `tests` CLI entry points additionally run under a separate supervising process, so a busy CLI event loop does not prevent sampling. The child retains its normal project/host admission and consumes the private parent marker before dispatch and evidence capture. If only the light budget fits, the CLI must stay within its 1 GiB ceiling; interruption exits 75 without a verdict. These controls do not change the daemon's limits below or govern commands launched independently by the user. `INTERLINKED_DISABLE_SESSION_END_JOBS=1` still opts out. In this repository, pre-push heavy commands also use the host lane through `scripts/run-resource-bounded.ts`. The supervisor retains ownership through child cleanup, and heavy commands wait at most ten minutes for admission so an existing five-minute daemon push check can finish first. Light diagnostics retain a five-second admission wait. The supervisor streams output, gives a heavy command's Node heap 80% of the admitted tree budget (3276 MiB at the 4 GiB ceiling; light commands 512 MiB), and limits common native thread pools. The heap share accommodates the repository's stable TypeScript compiler (about 2.9 GB of heap, measured 2026-10-02, past the former fixed 2560 MiB); the 4 GiB aggregate tree ceiling remains enforced independently by the resident-memory watcher. Resource interruptions exit 75 and block the push. For small diagnostic/test batches, `node --max-old-space-size=128 --import tsx scripts/run-resource-bounded.ts --light [args...]` uses a stricter 1 GiB tree ceiling, a 512 MiB Node heap ceiling, and 2 GiB of host headroom. It cannot run an oversized check successfully: the same monitor interrupts it. Pre-push keeps the heavy profile and its full exact-revision test/coverage contract. The default daemon V8 heap cap is 1536MB and the hard RSS recycle ceiling is 2048MB. `INTERLINKED_HARNESS_HEAP_MB` is accepted only when it is finite and at least 1; fractional values are floored, and every invalid value falls back to 1536. The CLI, hook self-heal path, and generated runtimes use that same parser. At the RSS ceiling the daemon does **not** spawn a successor beside its already-bloated heap and does not keep serving indefinitely. It first holds the project startup mutex under its own PID, then shuts down gracefully. Cold hooks continue their bounded deterministic guards, see the live owner during the short teardown, and do not spawn an overlapping daemon; after the old PID exits, dead-owner reclamation opens the same single-flight self-heal path. A valid `interlinked disable --reason …` stand-down marker suppresses that recovery entirely. The dominant measured spike was an append-only activity log that had grown past 1GiB and was being read into one JS string on daemon sequence checks. Live activity, daemon-ledger, and timeline-dedup readers now read byte-bounded tails directly. Explicit history/backfill commands may still scan complete logs; they are operator work, not the per-hook daemon path. Idle and heap-pressure shrink now drops every reconstructible PostTool cache together: mutation manifests, per-file external-check diagnostics, the TypeScript overlay service, and the trigram index's dirty layer. This bounds retained state from long multi-file/parallel-agent sessions; the next request rebuilds only what it needs. ### A missing or broken hook binary degrades reads and fails closed on writes (2026-08-31) The installed hook command accepts its baked binary only when it is a regular, non-empty file. When it is gone, empty, or fails to return an intentional allow/block status (unbuilt clone, moved checkout, interrupted build, corrupt JavaScript), its self-contained wrapper still parses the provider's native tool identity. Claude/Codex reserved read builtins and Cursor's dedicated `beforeReadFile` event proceed with exit 0 and a degraded warning. Ambiguous generic names on Copilot, Gemini, Cursor `preToolUse`, and MCP/custom events do not earn that exception. Mutating tools and unknown future tool names receive the provider's native deny response (not a fictional uniform exit-code contract), with the repair instruction on stderr. A shell command is never inferred read-only from command text, even when it looks like `cat` or `git status`; only the exact operator commands below escape. Claude's native `WorktreeCreate` also stays fail-closed. Non-gating events normalize runtime failures to exit 1 (a logged failure that does not claim a block). The standalone generated runtime has the same terminal boundary for the supported pair. Once a parseable Claude/Codex `PreToolUse` or `PermissionRequest` payload identifies the native event, an unexpected top-level exception emits that event's exact deny envelope; a non-gating exception emits no stdout and exits 1. Provider stdout is staged until the surrounding audit work succeeds, so a late exception discards the staged object before the terminal response and can never produce partial or duplicate JSON. The installed foreground wrapper remains the outer fail-closed boundary for experimental providers. Empty or malformed stdin cannot identify a native event and is treated as an invalid hook invocation rather than guessed into a provider decision. The foreground fail-closed wrapper keeps exact operator recovery commands executable before it invokes even a present runtime: bounded `harness start` / `restart`, `harness status [--json]`, read-only `doctor [--json]`, non-destructive `disable`, and `interlinked install-hooks --refresh --preserve-mode`. Exact `node dist/index.js` and `npx tsx src/index.ts` forms are recognized too; an arbitrary absolute `*/dist/index.js` is not. The two source-build spellings (`npm run build` and `node scripts/build-atomic-cli.mjs`) are a checkout-local exception only: the current directory must be the same checkout that owns the baked `dist/hook-entry.js` path, its package must be named `interlinked-cli`, and it must carry the canonical build declaration and script file. An application repo does not bypass its installed hook merely because it happens to use either build spelling. This prevents a missing or stale runtime from locking out its own diagnosis and repair. It is deliberately not a general shell allowlist: prefixes, suffixes, extra flags outside the bounded command grammar, pipes, and compound commands still run the normal hook and remain blocked. A matching string in a non-shell payload also runs the normal hook; warn-open events never get this escape. If neither the installed command nor a built checkout is available, reinstall or rebuild first, then run the preserve-mode refresh. Claude `PreToolUse` and `PermissionRequest` are supported. On PermissionRequest, Interlinked emits deterministic denies with Claude's dedicated `hookSpecificOutput.decision = { behavior: "deny", message }` object; that is distinct from PreToolUse's `permissionDecision`. Allow/ask abstain on stdout so Claude's configured policy and native user prompt retain authority. Non-blocking PermissionRequest diagnostics stay on stderr because that event does not accept generic `additionalContext`. Codex follows the same abstain-on-allow/ask rule through its provider-specific PermissionRequest response path. Codex `additionalContextLimit` applies only to `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, and `SubagentStart`. Permission decisions, Stop/SubagentStop continuation reasons and compaction diagnostics use separate output channels. Installing limits on those events causes Codex configuration warnings. Both install paths omit them; reinstalling also removes obsolete limits from Interlinked-owned handlers while preserving foreign hooks. Use the manifest-scoped refresh below for an existing adapter install. Changed project hook definitions may need renewed review in Codex `/hooks`. Stop/SubagentStop continuation is emitted only for an explicit harness `block`. Allowed or approval-requested diagnostics stay on exit-0 stderr without restarting the agent; Stop has no approval channel. Updating this translation requires the compiled hook runtime to be rebuilt or upgraded, not a hook-matcher change. Claude's installed `WorktreeCreate` hook is a deliberate hard stop: that native event replaces Claude's default Git behavior, and Interlinked fails it without returning a path. Across every client, the shared shell guard also blocks `git worktree add`; `list`, `remove`, and `prune` remain available. Interlinked uses a ban rather than a race-prone concurrent-count cap. Stale installed hooks (the pre-2026-08-28 SILENT fallback, a deregistered event entry, an old binary path) are repaired with `interlinked install-hooks --refresh --preserve-mode` — the hooks-only path (added 2026-08-29, hardened 2026-08-30). It re-renders ONLY the Interlinked-owned entries already in `installer-manifest.json` at their recorded scopes. A Codex install made before the 12-event `Interrupt` catalog also needs this refresh, followed by review in Codex `/hooks`: adding the event changes the definition hash and therefore trust state. A corrupt manifest refuses with the bytes preserved (strict per-row validation). Restore a trusted backup or repair it to the valid schema; do not delete it and then run refresh, because the missing ownership record also refuses and cannot identify which hooks are Interlinked-owned. Every file the run can touch — project AND $HOME-scope settings, Codex's config.toml, the manifest — is snapshotted (bytes + file mode) first; any install failure, any skipped target, or any failed final-state verification restores the whole snapshot and fails. That rollback covers HANDLED failures only — an OS crash mid-write cannot run the in-memory restore. Verification is STRUCTURAL (shared with the doctor `install drift` check): each expected hook entry exactly once at its native JSON path, any Interlinked-owned command outside the adapter's current render fails (the canonical ownership recognizer, so old binaries and legacy `.mjs` installs are caught whatever their quoting), and Codex's `[features] hooks = true` via the table-aware reader — duplicate `[features]` tables or duplicate assignments fail. The manifest itself is strictly validated: unknown runners, invalid scopes, prototype-chain path segments, duplicate runner rows, and any settings path that does not match the adapter's own derivation all read as CORRUPT, and install/uninstall/refresh refuse rather than overwrite the evidence. Uninstall removes hooks by owned-entry recognition, never by stored array index — a user hook added after install is safe wherever it sits. Ownership is shell-position parsed. An adapter entry needs the exact `hook-entry.js` / `interlinked-hook` basename, a registered `--runner`, and a present `--event`; legacy `.mjs` ownership needs the reserved `.interlinked/hooks/interlinked-activity.mjs` path or its exact generated assignment form. A command that only prints, comments on, looks like, or invokes a user script with the same basename without those required shapes is not claimed. Project attribution reads the invoked script path, not text elsewhere in the command. It NEVER writes enforcement mode, cloud config, or guard rules. Do NOT repair with a plain `interlinked enable` or a plain `install-hooks` — both also select and write an enforcement mode (the `--mode` default is "balanced"). `--dry-run` previews; a second run reports `unchanged`. A normal install replaces ownership only after the adapter finishes its semantic post-install step. A selected runner that is skipped (for example, malformed project settings) cannot purge a working hook at another scope. If a replacement's post-install step fails, its attempted settings file is restored and the prior one-row manifest entry remains authoritative; a first-time failure has no prior state to restore, so its partial hook and failed manifest row remain together for `uninstall-hooks` to remove. In either case the command reports failure rather than claiming the runner is ready. ## Diagnosing problems Project E2E `interlinked tests e2e doctor` retains report version 1 and adds optional `evaluation: "not-evaluated"` and `prerequisites` fields to dependent checks. A ledger failure leaves receipt validation unevaluated; unresolved contract references prevent complete acceptance/toolchain success. Known missing preparation tools can still fail independently. POSIX command inspection checks regular executable files under the runner's PATH/cwd. A same-suite artifact expected after preparation is a warning, not verified availability; preparation commands never receive that exemption. Runtime placeholders or unsupported platform lookup stay unverified. The doctor does not execute preparation. ### Hook capability inventory and experimental clients `interlinked harness capabilities --json` includes the sourced ecosystem catalog, adapter event/control declarations, installer manifest, last provider emission and filesystem coverage state. `selected` describes the adapter's subscription policy; inspect the manifest to establish installation. An emitted response or matching definition hash does not certify that the provider enforced it. Native enforcement remains `unmeasured` until a provider/version/mode conformance run establishes it. Factory Droid (`factory-droid`), Windsurf (`windsurf`), Google Antigravity (`antigravity`) and Crush (`crush`) are explicit experimental adapters through `interlinked install-hooks --runner `. They require a built hook-entry runtime. They are separate from the seven `enable --clients` integrations and do not expand skill installation targets. Their command wrappers currently target POSIX shells; Windows shell behavior and native headless behavior are not certified. Antigravity uses `.agents/hooks.json`, separate from Gemini CLI's `.gemini/settings.json`. Claude FileChanged installs without a static matcher. SessionStart, CwdChanged and FileChanged responses publish the complete current `watchPaths` list. FileChanged is an observation after a write; it cannot deny or undo that write. PostToolBatch can cancel the loop before the next model request. Codex does not acquire either native event merely because it uses Claude-shaped payloads; daemon filesystem observation supplies a separate local fallback. Expanded boundary handling requires the compiled runtime. The adapter installer refuses a generated compatibility script instead of registering events that it cannot serve. Build the checkout and refresh hooks; previously installed legacy scripts retain their older compatibility behavior. The public `interlinked-cli/hook-bridge` export provides an application-owned Think tool bridge and a Durable Object storage journal adapter. It is not installed with coding-client command hooks. See the repository's `docs/hook-normalization.md` for its admission, replay and deployment contract. `interlinked doctor` is the first stop. It runs local + system + server checks and **exits non-zero if any check fails**. `--fix` repairs common drift (regenerates a drifted hook script, safely refreshes Interlinked-owned skill copies, strips malformed permission rules, migrates legacy config). ```bash interlinked doctor # diagnose interlinked doctor --fix # auto-repair what it safely can interlinked context --json # show the effective merged config interlinked env # list supported env vars + current values ``` **What doctor's client rows do and do not prove (2026-08-30).** Doctor checks the hook-config location for **every** client in the settings registry — Claude Code, Codex, Copilot, Gemini, Cursor, OpenCode, and Pi — reading each path from that one registry instead of restating it (the hardcoded copy it replaced pointed Codex at `.codex/config.toml`, which holds only the feature flag, so doctor warned that every CORRECT Codex install was missing its hooks). Consequences worth knowing: - **Codex gets three rows, deliberately.** The hooks row reads `.codex/hooks.json`; a second row reads the `[features] hooks = true` gate in `config.toml`, **table-aware** — a `hooks = true` under some other table does not count. Installed hooks with the flag off are reported as INERT, not missing, because those are different repairs. The third row checks `.interlinked/hook-runtime.json`: it passes only after Codex executes the current `hooks.json` definition hash. If it warns, open `/hooks`, review the definition, and run a hooked action. - **The flag writer canonicalizes to a single `hooks` key.** Whatever mix of `hooks` / `codex_hooks`, true / false, duplicated or commented, `[features]` already holds, the writer leaves a single `hooks = true` — comments, unrelated tables and the file's line endings intact. Duplicate TOML keys are a parse error, so the old "add a canonical key beside what we found" behavior could report a successful migration for a config Codex then refused. - **A failed post-install now fails the install.** Codex is the one runner whose hooks.json is inert without a second write, so when that write throws, `install-hooks` reports `ok: false`, lists the runner under `post_install_failures`, marks the manifest entry `post_install: "failed"`, exits non-zero, and `enable` reports the client as not installed. It used to log one stderr line and report success. - **A passing hooks row means the config is present, not that execution is verified.** The Codex execution row is the stronger live proof for the current definition. Codex installs all twelve native events from the shared capability catalog. Copilot and Gemini adapter normalization and the Cursor duplicate-invocation question are open work; treat those three runners as unproven. OpenCode/Pi have managed-bridge execution tests, but their narrower native APIs still make them experimental rather than Claude/Codex-equivalent. Likewise `enable --dry-run`'s per-client **event counts are computed from the adapter that performs the install** (they were prose literals that had drifted, so the preview promised numbers the install did not deliver). They count REGISTRATIONS, not verified provider capabilities. What "healthy" looks like in `doctor`: config dir + both config files present, hook script present, per-client "Hooks installed", and **Harness server: Running (PID …) -- socket answering**. A zombie is a **`fail`** here, never a pass. Hook detection shares one ownership predicate with the installers, so an adapter (`hook-entry.js`) install is recognised as installed rather than reported missing. A manifest-tracked hook binary that no longer exists is a **`fail`**, even when the settings document still has the expected entries. A missing token on a non-localhost server is a `fail`; on localhost it is only a `warn` (dev mode allows unauthenticated). ### When the daemon will not stay up A daemon that fails **before** binding its socket exits **78** (`EX_CONFIG`) and appends an `exit` row with `reason: "startup-failed"` to `.interlinked/daemon-events.jsonl`. It no longer lingers as a zombie. Read that ledger before theorising — it separates a failed bind (78) from a graceful stop, a lost ownership race (**0**, orderly, not a failure), and a crash. ```bash tail -n 20 .interlinked/daemon-events.jsonl # why did it leave? interlinked harness reap # list orphan daemons (--force kills) interlinked harness restart # the usual fix ``` Bind attempts are bounded with backoff, and a socket that **answers** is never unlinked — a live incumbent wins and the newcomer exits instead of stomping it. Only a silent, stale socket file is cleared and retried. **Dev loop after editing the CLI source:** `interlinked reload` rebuilds the CLI in its own checkout, refreshes this repo's hooks and deployed skills, and restarts the daemon **only if something the daemon executes changed**. The package build is runtime-preserving, not a whole-directory transaction. One repository-scoped lease prevents concurrent compiler runs; an equivalent waiter coalesces after the owner finishes. The owner bundles, repairs declarations, copies assets, and checks every shipped entry in a sibling staging directory, then refuses publication if a product build input changed meanwhile. Publishing atomically renames shared chunks/assets first and runtime entrypoints last (`hook-entry.js` last of those and therefore the handled-failure runtime commit point). The entry phase snapshots existing entries and rolls back anything already replaced if a later entry fails. This is not crash-atomic across several entry renames; after process/OS loss, each old-or-new entry remains independently runnable because both generations' hashed chunks stay present, and the next build converges the entries. `dist/` itself is never removed, and prior hashed chunks remain available to a running daemon's later lazy imports. A fault during shared-file publication can therefore leave harmless new immutable files in `dist/`, but it leaves the previous entry generation in place. Fix the reported error and retry instead of reconstructing `dist/` by hand. ## Config & environment - **`.interlinked/config.json`** (committed): `server_url`, `default_project`, `mode` (operational tier), `skip_paths`, `pii_patterns`, nested `harness` feature flags. - **`.interlinked/config.local.json`** (gitignored): `access_token`, `agent_name`, `workspace_id`, `sync_mode`, `active_server` + `servers` map, `guard_mode`, `data_dir`. - **`.interlinked/semantic.json`** (committed): optional semantic-index enablement, exact pinned model reference, and source/test include policy. - **`.interlinked/semantic.local.json`** (gitignored): local-only CPU/runtime topology. Remote URLs, API tokens, and cloud fallbacks are rejected by the v1 schema. - **Env overrides** (win over both files): `INTERLINKED_SERVER_URL`, `INTERLINKED_ACCESS_TOKEN` (alias `INTERLINKED_TOKEN`), `INTERLINKED_AGENT_NAME`, `INTERLINKED_WORKSPACE_ID`, `INTERLINKED_SYNC_MODE`, `INTERLINKED_HOME` (relocates the whole config dir), `INTERLINKED_DATA_DIR`, `INTERLINKED_CLIENTS` (non-interactive bootstrap only). `enable`, `adopt`, `doctor`, metrics, hooks, and search do not auto-download embedding weights. The experimental semantic commands also require compatible local `llama-embedding` and `llama-tokenize` executables (override their command names only in `semantic.local.json`). Model weights live in the platform user cache; project vectors live under the gitignored `.interlinked/index/functions/` directory and are never synced. **Two different `mode` commands — do not conflate them:** - `interlinked mode ` → per-check **policy** preset → `check-policy.json`, PLUS the preset's guard posture (TDD/test-first tier, per-edit coverage + debt mode, session-end nudges, commit cadence) into `guard-rules.json`. Since 2026-08-30 the loader actually honors that posture: the team tier merges the whitelisted safe fields (booleans + the three posture enums) and drops runtime knobs like `budget_ms`. `--local` writes BOTH halves to the personal tier (`check-policy.local.json` + `guard-rules.local.json`) — a personal mode switch never edits committed team policy. The wizard's scope (`diff_aware.enabled`) and dead-code writes load through the same whitelist. The mode switch is transactional in BOTH directions (a refused guard merge writes neither file; a failed check-policy write rolls the guard file back), and the three posture enums are value-validated at the final loader boundary for BOTH tiers — an invalid value is dropped (built-in default applies) and `interlinked doctor` names the file, field, and value. - `interlinked harness mode ` → operational **timeout tier** → `config.json` `mode` + regenerates the hook. Requires `harness restart` to take effect. ## Auth & server (optional) ```bash interlinked login --server https://your-server.dev # OAuth PKCE (opens browser) interlinked login --token "$INTERLINKED_TOKEN" # CI / headless interlinked attach --agent my-bot --auto # link identity + workspace interlinked logout [--all] ``` - **localhost/127.0.0.1 = dev mode → auth skipped** (`setup`/`init` skip login for local servers). - Token resolution: CLI token in `config.local.json` → auto-refresh → Claude Code credential fallback (`~/.claude/.credentials.json`). - Sync modes: `realtime` (default; per-event post + session-end batch), `manual` (per-event only), `local` (no server posts at all). ## Turning it off | Command | Effect | |---|---| | `interlinked disable [--reason ] [--until ] [--team]` | **Non-destructive** stand-down: records a marker + stops the daemon. Hooks and config stay. Re-arm with `interlinked enable`. | | `interlinked disable --uninstall [--keep-config]` | Destructive teardown: removes hooks + the installed skills **and deletes the `.interlinked/` config dir** — pass `--keep-config` to keep the config. | | `interlinked reset --force` | **Nuclear**: delete the entire `.interlinked/` dir and strip hook entries. Irreversible; `--force` required. | | `interlinked uninstall-hooks` | Remove **only** what `install-hooks` recorded in its manifest. | > A valid stand-down marker suppresses hook self-heal. If `disable` reports the old daemon is > still running, run `interlinked harness stop`: that already-running process may continue to > evaluate calls until it exits, but hooks will not resurrect it afterward. ## Gotchas - **`disable` is non-destructive by default now** — bare `disable` just stands down; use `--uninstall` (or `reset --force`) for real teardown. - **`clean` defaults to dry-run**; `reset` requires `--force`. - **Install paths have non-interchangeable uninstall semantics.** `uninstall-hooks` only cleans an `install-hooks`-style install; use `disable --uninstall` / `reset` to clean an `enable` install. - **Claude Code merge-up dedup:** `enable` refuses to install Claude hooks when an ancestor `.claude/settings.json` already has them (would double-fire) — run `enable` from that ancestor. - **Daemon discovery stops at a Git boundary.** A hook invoked in a linked worktree or submodule uses that worktree's `.interlinked/` runtime and never borrows a daemon socket from the parent checkout. Ordinary monorepo subdirectories still discover the nearest ancestor daemon. - **Gemini is a compatibility lane, not the Antigravity adapter.** Consumer Gemini CLI service ended in June 2026, while enterprise and paid API-key Gemini CLI use remain supported. The current `gemini` client installs Gemini CLI hooks/skills; do not treat it as Antigravity. - **OpenCode and Pi installs are managed source bridges.** Project scope writes `.opencode/plugins/interlinked.ts` or `.pi/extensions/interlinked.js` as an Interlinked-owned whole file; user scope uses `~/.config/opencode/plugins/interlinked.ts` and `~/.pi/agent/extensions/interlinked.js`. Install refuses to overwrite a foreign file at that path, and uninstall preserves a bridge whose bytes changed after install. A loaded bridge writes its provider row to `.interlinked/hook-runtime.json`, but only Codex currently has a dedicated doctor trust-verification row. Restart OpenCode after install (its plugin trust is implicit). In Pi, run `/reload` or restart and approve the project-extension trust prompt. - **Their native parity is intentionally bounded.** OpenCode's stable tool-before hook cannot open confirmation, so `ask` denies; permission bus and `session.idle`/Stop are observation-only. Pi prompts through `ctx.ui.confirm` when interactive and denies headless; `user_bash` gates direct shell commands as well as model tool calls. Neither exposes dedicated native MCP, subagent, or worktree lifecycle hooks. The shared shell rule still blocks `git worktree add`. - **`reload` needs a source checkout** — it rebuilds the CLI checkout the running binary resolves to (typically a `~/.local/bin` symlink), not the current repo. - `--json` support is per-command; unknown flags error. `doctor` takes only `--fix`/`--json`. ## Related skills - **interlinked-verify** — project e2e adoption after enable: `interlinked tests e2e discover --out report.json`, review, `tests e2e adopt --from report.json` (advisory unless `--mode required`), `tests e2e doctor`. Discovery proposes; it never accepts inferred behavior. - **interlinked-harness** — what the guard blocks and how to respond when a tool call is refused. - **interlinked-observability** — inspect the activity the hooks capture (`status`, `activity`, `logs`). - **interlinked-coordination** — the optional server-backed side (tasks, messages, workspaces). - **interlinked-semantic-index** — explicitly install a model and build/query the local function index.