Read the script
#!/usr/bin/env bash
# trace-mcp-guard v0.20
# REQUIRES: trace-mcp >= 1.32.7 (status JSON sentinel introduced in this version)
#
# v0.20 changes (GH #1481 — linked git worktrees):
# - A linked worktree is served from the MAIN checkout's index, so the index
# holds the main version of every file the branch changed. The guard had no
# worktree notion: the sentinel is hashed by the main root, the walk up from
# a worktree path never reaches it, and the session read as "server not
# running" — strict routing off for the whole session.
# - The hook now detects a linked worktree (pure bash, reads .git metadata,
# no subprocess in a main checkout) and, when no sentinel is found for the
# worktree path (or only a stale one), falls back to the main checkout's
# sentinel and consultation markers.
# - Read (and Grep with a file path) on a file the worktree changed relative
# to the main checkout's HEAD — `git diff --name-only <main HEAD>` plus
# untracked files, cached TRACE_MCP_GUARD_WORKTREE_TTL seconds (default 5)
# — is allowed without a consultation: trace would answer with the main
# version. Files modified on disk since the cache was written count too.
# Everything else is routed exactly as in a main checkout.
# - Outside a linked worktree nothing changes.
#
# v0.19 changes (TRA-1791 — unresolvable-session fallback):
# - The guard knew only "daemon alive / daemon dead". A session whose MCP
# client connected with an unusable root (cwd=/ → dangerous hint dropped
# → ambiguous over N registered projects) has a LIVE daemon and can never
# earn a consultation marker — every trace call 400s — so each code Read
# stayed BLOCKED until the 5-deny auto-degrade tripped, expired, and
# tripped again. The loop users felt as "the guard randomly blocks Reads".
# - The daemon now persists unresolvable /mcp resolutions to
# <state home>/status/trace-mcp-daemon.json (daemon-global: an ambiguous
# failure has no project to attribute it to). When that file shows a
# FRESH unresolvable resolution, this session holds ZERO consultation
# markers, and the session already suffered UNRESOLVABLE_MIN_DENIES
# (default 2) denies, the hook allows with a warning instead of denying:
# consultation is impossible here, not skipped. First denies stay strict
# so healthy sessions keep the "call get_outline first" nudge, and a
# per-session latch keeps the session usable (no re-block loop) until a
# marker proves the channel healed. No daemon file (older server) behaves
# exactly as v0.18.
#
# v0.18 changes (TRA-1088 — find the sentinel from a subdirectory):
# - Resolves the project root by walking up from the cwd to the nearest
# ancestor a server has claimed, instead of demanding that the cwd itself
# be the root the MCP client launched with. Working one directory down —
# a checked-out repo, a monorepo package — made the guard report a live
# session as dead and wave every Read/Grep through.
# - `/` is never accepted as that ancestor: a registry row for `/` is a known
# pathology (TRA-37) and sits on the walk of every path.
#
# v0.17 changes (TRA-763 — StateEngine discovery hint):
# - Counts guarded tool calls per session and, on the 30th (default,
# TRACE_MCP_STATE_HINT_TURNS), emits one advisory additionalContext
# pointing at load_tools + trace_state_init. TRA-724 measured the
# break-even for that loop at turns 25-28; the seven schemas are kept off
# the default `minimal` preset for the short sessions that lose, which
# left the feature unreachable in the long ones that win.
# - Silent when the channel is dead, when TRACE_MCP_PRESET already
# advertises the tools, when state.db has been written since the session
# started, and after the one shot. Never blocks.
#
# v0.16 changes (TRA-869 — sentinels moved out of $TMPDIR):
# - The heartbeat, status, consultation-marker and bypass paths are read from
# <state home>/status/ first, $TMPDIR second. $TMPDIR is per-process: the
# server is spawned by the MCP client and this hook by the agent harness,
# and on macOS they routinely hold different values. Measured live: the
# server refreshed /var/folders/.../T/trace-mcp-alive-<hash> every 5s while
# the hook looked in /tmp/multica-task-<id>/, so every call reported
# "trace-mcp server not running" and degraded to the Read/Grep fallback,
# against a healthy connected session.
# - The $TMPDIR fallback keeps a server older than that fix discoverable.
#
# v0.15.1 changes (TRA-845 — Bash branch had no liveness fallback):
# - Read, Grep and Glob all degrade to allow-with-warning when trace-mcp is
# unreachable (no heartbeat, stale heartbeat, stalled channel, transport
# mismatch, manual or auto bypass). The Bash branch never checked any of
# it, so with the daemon stopped `grep -rn foo src/`, `cat src/x.ts`,
# `ls src/`, `git diff src/x.ts` and `cmd < src/x.ts` were hard-denied
# while the tools they redirect to could not answer either.
# - Each of those six deny sites now calls bash_fallback_if_unavailable
# first. Ordinary Bash calls (builds, tests, git status) stay silent, and
# the .env rule stays unconditional — it is a secrets rule, not a
# navigation-cost tradeoff.
#
# v0.15 changes (guard v2 — TRA-711, navigation streak gate):
# - The guard no longer intervenes on an isolated navigation call. TRA-705
# measured the trace path at 1.45x the cost of a bare grep agent on a light
# navigation question with identical correctness (27/30 vs 27/30) — routing
# that question through us is a measured regression, not a saving. The win
# is on multi-step work (1.39x our way), so the guard now waits for the
# session to actually be crawling.
# - Navigation-class denies (Read/Grep/Glob/Bash code exploration/git
# show|diff|log -p) now fire from the TRACE_MCP_GUARD_NAV_MIN'th