Skip to content
Development
Hook

Hooks

What trace-mcp runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

From plugin
trace-mcp
2034 skills14 hooks

Claude Code

> /plugin marketplace add nikolai-vysotskyi/trace-mcp
> /plugin install trace-mcp@trace-mcp

Where it lives

  • hooks/trace-mcp-guard-md-tour.ps1GitHub
    Read the script
    # trace-mcp-guard-md-tour.ps1 v0.9.0
    # Windows helper for trace-mcp-guard.cmd - implements the .md doc-tour
    # detection mirrored from the bash hook (v0.9).
    #
    # Called by trace-mcp-guard.cmd on PreToolUse Read events for .md files.
    # Behavior:
    #   - If the file is NOT in a source-tree directory, exit 0 silently
    #     (caller treats empty stdout as "allow").
    #   - If the file IS in a source-tree directory, increment a per-session
    #     counter. When the counter reaches TRACE_MCP_GUARD_MD_HINT_THRESHOLD
    #     (default 3), emit a full PreToolUse JSON with additionalContext
    #     suggesting get_feature_context / get_task_context. Read still passes -
    #     this is a hint, not a block.
    #
    # Inputs (environment variables):
    #   TMG_FILE     - absolute file path being read
    #   TMG_SESSION  - session id (per-session state lives under
    #                  $env:TEMP\trace-mcp-reads-<session>\)
    #   TMG_ROOT     - project root (pwd of the Claude Code session)
    
    $ErrorActionPreference = 'SilentlyContinue'
    
    $filePath = $env:TMG_FILE
    $sessionId = $env:TMG_SESSION
    if (-not $filePath -or -not $sessionId) { exit 0 }
    
    # Normalize backslashes so the same regexes work as in the bash hook.
    $norm = $filePath -replace '\\', '/'
    
    $isMd = $norm -match '(?i)\.md$'
    if (-not $isMd) { exit 0 }
    
    $inSource = $norm -match '(?i)/(src|lib|packages|apps?|server|client|pkg|internal|modules|services|pipelines|cmd|tests?|specs?|features?)/'
    $excluded = $norm -match '(?i)/(docs?|node_modules|vendor|dist|build|\.git|target|out)/'
    if (-not $inSource -or $excluded) { exit 0 }
    
    $tmp = $env:TEMP
    if (-not $tmp) { $tmp = [System.IO.Path]::GetTempPath().TrimEnd('\','/') }
    $readsDir = Join-Path $tmp ("trace-mcp-reads-" + $sessionId)
    if (-not (Test-Path $readsDir)) {
        New-Item -ItemType Directory -Path $readsDir -Force | Out-Null
    }
    $counterFile = Join-Path $readsDir '.md-tour-count'
    
    $count = 0
    if (Test-Path $counterFile) {
        $raw = (Get-Content -LiteralPath $counterFile -Raw).Trim()
        [void][int]::TryParse($raw, [ref]$count)
    }
    $count = $count + 1
    Set-Content -LiteralPath $counterFile -Value "$count" -NoNewline
    
    $threshold = 3
    if ($env:TRACE_MCP_GUARD_MD_HINT_THRESHOLD) {
        [void][int]::TryParse($env:TRACE_MCP_GUARD_MD_HINT_THRESHOLD, [ref]$threshold)
    }
    
    if ($count -lt $threshold) { exit 0 }
    
    # Compute repo-relative path for the hint message.
    $rel = $filePath
    $root = $env:TMG_ROOT
    if ($root) {
        $rootWithSep = $root.TrimEnd('\','/') + [System.IO.Path]::DirectorySeparatorChar
        if ($filePath.StartsWith($rootWithSep, [System.StringComparison]::Ordinal)) {
            $rel = $filePath.Substring($rootWithSep.Length)
        }
    }
    $rel = $rel -replace '\\', '/'
    
    $hint = "trace-mcp guard: ${count}x .md reads inside source dirs this session - looks like a doc tour. For per-feature docs co-located with code, get_feature_context / get_task_context is usually faster than reading docs file-by-file. Reading $rel is allowed; this is a hint, not a block.`nAlternatives:`n- get_feature_context { ""description"": ""what these docs describe"" }`n- get_task_context { ""task"": ""what you are working on"" }`n- search { ""query"": ""keyword"", ""file_pattern"": ""**/*.md"" } - find specific doc by name"
    
    $payload = [ordered]@{
        hookSpecificOutput = [ordered]@{
            hookEventName = 'PreToolUse'
            additionalContext = $hint
        }
    }
    $payload | ConvertTo-Json -Depth 4 -Compress
    exit 0
    
  • hooks/trace-mcp-guard-read.ps1GitHub
    Read the script
    # trace-mcp-guard-read.ps1 v0.11.0
    # Windows helper for trace-mcp-guard.cmd - implements the Read-handler repeat-read
    # dedup logic (per-session allowed read counter with mtime reset).
    #
    # Called by trace-mcp-guard.cmd on PreToolUse Read events. Writes one decision
    # string to stdout:
    #
    #   ALLOW        - allow the read (caller should exit 0 silently)
    #   LIMIT:<n>    - deny: already read <n> times this session (caller emits
    #                  the "Already read" deny JSON)
    #   DENY_FIRST   - deny: first-time friction cycle (caller emits the generic
    #                  "Use trace-mcp" deny JSON; retry will ALLOW)
    #
    # Inputs are taken from environment variables (simpler than cmd arg quoting):
    #   TMG_FILE       - absolute file path being read
    #   TMG_SESSION    - session id
    #   TMG_ROOT       - project root (pwd of the Claude Code session)
    #   TMG_OFFSET     - Read offset parameter (if set, targeted pre-Edit read -> ALLOW)
    #   TMG_LIMIT      - Read limit parameter  (if set, targeted pre-Edit read -> ALLOW)
    #
    # This script is side-effecting: it writes state files under
    #   $env:TEMP\trace-mcp-reads-<session>\<file-hash>        ("count:mtime")
    # and creates/removes deny markers under
    #   $env:TEMP\trace-mcp-guard-<session>\<file-hash>
    
    $ErrorActionPreference = 'SilentlyContinue'
    
    $filePath  = $env:TMG_FILE
    $sessionId = $env:TMG_SESSION
    $projectRoot = $env:TMG_ROOT
    $tmgOffset = $env:TMG_OFFSET
    $tmgLimit  = $env:TMG_LIMIT
    $tmp = $env:TEMP
    if (-not $tmp) { $tmp = [System.IO.Path]::GetTempPath().TrimEnd('\','/') }
    
    if (-not $filePath -or -not $sessionId) {
        Write-Output 'ALLOW'
        exit 0
    }
    
    # Targeted pre-Edit reads (offset or limit present) - always allow.
    # Read-before-Edit must keep working even under strict enforcement.
    if ($tmgOffset -and $tmgOffset -ne '') {
        Write-Output 'ALLOW'
        exit 0
    }
    if ($tmgLimit -and $tmgLimit -ne '') {
        Write-Output 'ALLOW'
        exit 0
    }
    
    $REPEAT_READ_LIMIT = 2
    
    function Sha256Hex([string]$text) {
        $sha = [System.Security.Cryptography.SHA256]::Create()
        try {
            $bytes = [System.Text.Encoding]::UTF8.GetBytes($text)
            return [System.BitConverter]::ToString($sha.ComputeHash($bytes)).Replace('-','').ToLower()
        } finally {
            $sha.Dispose()
        }
    }
    
    $fileHash = Sha256Hex $filePath
    
    # Read state dir & path
    $readsDir = Join-Path $tmp ("trace-mcp-reads-" + $sessionId)
    if (-not (Test-Path $readsDir)) {
        New-Item -ItemType Directory -Path $readsDir -Force | Out-Null
    }
    $readState = Join-Path $readsDir $fileHash
    
    # Current mtime as ticks (cross-version stable)
    $curMtime = '0'
    if (Test-Path $filePath) {
        try {
            $curMtime = (Get-Item -LiteralPath $filePath).LastWriteTimeUtc.Ticks.ToString()
        } catch {
            $curMtime = '0'
        }
    }
    
    # Load previous state
    $prevCount = 0
    $prevMtime = ''
    $hadState = $false
    if (Test-Path $readState) {
        $hadState = $true
        $raw = (Get-Content -LiteralPath $readState -Raw).Trim()
        $parts = $raw -split ':', 2
        if ($parts.Length -ge 1) {
            [void][int]::TryParse($parts[0], [ref]$prevCount)
        }
        if ($parts.Length -ge 2) {
            $prevMtime = $parts[1]
        }
    }
    
    # Reset count on mtime change (Edit/Write happened).
    if ($curMtime -ne $prevMtime) {
        $prevCount = 0
    }
    
    # Limit exceeded -> deny.
    if ($prevCount -ge $REPEAT_READ_LIMIT) {
        Write-Output ("LIMIT:" + $prevCount)
        exit 0
    }
    
    # Consultation marker: if trace-mcp already touched this file, allow unconditionally
    # (still increments the counter so the limit eventually triggers).
    if ($projectRoot) {
        $projectHash = (Sha256Hex $projectRoot).Substring(0, 12)
        $relPath = $filePath
        $rootWithSep = $projectRoot.TrimEnd('\','/') + [System.IO.Path]::DirectorySeparatorChar
        if ($filePath.StartsWith($rootWithSep, [System.StringComparison]::Ordinal)) {
            $relPath = $filePath.Substring($rootWithSep.Length)
        }
        $relPath = $relPath -replace '\\', '/'
        $consultedHash = Sha256Hex $relPath
        # Markers live under the state home; %TEMP% is the pre-TRA-869 location and
        # is still checked because the server writes both while old hooks exist.
        $traceStateHome = $env:TRACE_MCP_DATA_DIR
        if (-not $traceStateHome) { $traceStateHome = Join-Path $env:USERPROFILE '.trace' }
        $stateConsultedFile = Join-Path (Join-Path (Join-Path $traceStateHome 'status') ("trace-mcp-consulted-" + $projectHash)) $consultedHash
        $consultedDir = Join-Path $tmp ("trace-mcp-consulted-" + $projectHash)
        $consultedFile = Join-Path $consultedDir $consultedHash
        if ((Test-Path $stateConsultedFile) -or (Test-Path $consultedFile)) {
            $newCount = $prevCount + 1
            Set-Content -LiteralPath $readState -Value ("{0}:{1}" -f $newCount, $curMtime) -NoNewline
            Write-Output 'ALLOW'
            exit 0
        }
    }
    
    # Already tracked this session (even after mtime reset) -> skip first-time friction.
    if ($hadState) {
        $newCount = $prevCount + 1
        Set-Content -LiteralPath $readState -Value ("{0}:{1}" -f $newCount, $curMtime) -NoNewline
        Write-Output 'ALLOW'
        exit 0
    }
    
    # First-time deny-marker cycle: first attempt denies, second attempt (retry) allows.
    $denyDir = Join-Path $tmp ("trace-mcp-guard-" + $sessionId)
    if (-not (Test-Path $denyDir)) {
        New-Item -ItemType Directory -Path $denyDir -Force | Out-Null
    }
    $denyMarker = Join-Path $denyDir $fileHash
    
    if (Test-Path $denyMarker) {
        Remove-Item -LiteralPath $denyMarker -Force
        Set-Content -LiteralPath $readState -Value ("1:" + $curMtime) -NoNewline
        Write-Output 'ALLOW'
        exit 0
    }
    
    New-Item -ItemType File -Path $denyMarker -Force | Out-Null
    Write-Output 'DENY_FIRST'
    exit 0
    
  • hooks/trace-mcp-guard.shGitHub
    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
  • hooks/trace-mcp-hidden-run.ps1GitHub
    Read the script
    # trace-mcp-hidden-run v0.1.0 (Windows)
    # Hidden launcher shim for trace-mcp Claude Code hooks.
    #
    # Problem: registering a hook as `cmd /c "...cmd"` makes Windows allocate a
    # visible console window for every invocation. During agentic editing the
    # PostToolUse reindex hook fires on every Edit/Write/MultiEdit, so dozens of
    # console windows flash per minute (issue #230).
    #
    # Fix: register hooks as
    #   powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass \
    #     -WindowStyle Hidden -File "<dir>\trace-mcp-hidden-run.ps1" "<dir>\<hook>.cmd"
    # PowerShell launched with -WindowStyle Hidden owns a hidden console; the child
    # cmd inherits that hidden console instead of allocating a fresh visible one, so
    # nothing flashes. This shim runs the existing, tested .cmd hook logic verbatim
    # via `cmd /c`, forwarding stdin (the PostToolUse JSON payload), relaying
    # stdout/stderr, and propagating the exit code.
    #
    # Managed by trace-mcp - do not edit by hand. Re-run `trace-mcp init` to refresh.
    
    #Requires -Version 5.1
    
    $ErrorActionPreference = 'SilentlyContinue'
    
    # The target .cmd hook to run is passed as the first positional argument.
    $target = $args[0]
    if (-not $target) {
        # No target given: nothing to run. Exit cleanly so a stray registration
        # never blocks a tool call.
        exit 0
    }
    
    # Read the hook payload (PostToolUse/PreToolUse JSON etc.) from stdin so it can
    # be piped through to the child .cmd, which parses tool_name / file_path from it.
    $stdin = [Console]::In.ReadToEnd()
    
    # Run the .cmd via cmd.exe /c inside this hidden console. Piping $stdin forwards
    # the JSON payload; stdout/stderr are inherited so any hook output (e.g. the
    # SessionStart wake-up context) reaches Claude Code unchanged.
    $stdin | & cmd.exe /c "`"$target`""
    
    exit $LASTEXITCODE
    
  • hooks/trace-mcp-launcher.ps1GitHub
    Read the script
    # trace-mcp-launcher v0.6.16 (Windows)
    # Stable shim backend: resolves node + cli.js at runtime from launcher.env,
    # with a probe fallback for nvm-windows/nvs/Volta/system installs.
    # Managed by trace-mcp - do not edit by hand. Re-run `trace-mcp init` to refresh.
    
    #Requires -Version 5.1
    
    $ErrorActionPreference = 'Stop'
    
    # Determine $TraceHome:
    # 1. Explicit TRACE_MCP_HOME or TRACE_MCP_DATA_DIR override.
    # 2. Sibling directory of this shim: installed at <TraceHome>\bin\trace.cmd
    #    and trace-mcp-launcher.ps1. If launcher.env or .config.json exists in
    #    the parent directory, use it. This survives modified USERPROFILE or isolated homes.
    # 3. $USERPROFILE\.trace
    # 4. $USERPROFILE\.trace-mcp (legacy)
    $TraceHome = ''
    if ($env:TRACE_MCP_HOME) {
        $TraceHome = $env:TRACE_MCP_HOME
    } elseif ($env:TRACE_MCP_DATA_DIR) {
        $TraceHome = $env:TRACE_MCP_DATA_DIR
    } else {
        if ($PSScriptRoot) {
            $candidate = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
            if ((Test-Path -LiteralPath (Join-Path $candidate 'launcher.env') -PathType Leaf) -or (Test-Path -LiteralPath (Join-Path $candidate '.config.json') -PathType Leaf)) {
                $TraceHome = $candidate
            }
        }
        if (-not $TraceHome -and $env:USERPROFILE) {
            $defaultTrace = Join-Path $env:USERPROFILE '.trace'
            if ((Test-Path -LiteralPath $defaultTrace -PathType Container) -or -not (Test-Path -LiteralPath (Join-Path $env:USERPROFILE '.trace-mcp') -PathType Container)) {
                $TraceHome = $defaultTrace
            } else {
                $TraceHome = Join-Path $env:USERPROFILE '.trace-mcp'
            }
        }
    }
    if (-not $TraceHome) { $TraceHome = Join-Path $env:USERPROFILE '.trace' }
    
    $env:TRACE_MCP_HOME = $TraceHome
    $env:TRACE_MCP_DATA_DIR = $TraceHome
    
    $ConfigPath = Join-Path $TraceHome 'launcher.env'
    $LogPath    = Join-Path $TraceHome 'launcher.log'
    
    # Rotate once per invocation, before the first append (TRA-702). Mirrors
    # rotate_log in trace-mcp-launcher.sh. Bounds the log at 2 x the limit across
    # both generations; without it the file only ever grew.
    $LogMaxBytes = 5242880
    try {
        # Inside the try on purpose: $ErrorActionPreference is 'Stop', so a
        # non-numeric override would throw on the cast and abort the whole shim
        # before it ever execs node - a logging knob must never cost a start.
        if ($env:TRACE_MCP_LOG_MAX_BYTES) { $LogMaxBytes = [int64]$env:TRACE_MCP_LOG_MAX_BYTES }
        $existing = Get-Item -LiteralPath $LogPath -ErrorAction SilentlyContinue
        if ($existing -and $existing.Length -gt $LogMaxBytes) {
            Move-Item -LiteralPath $LogPath -Destination "$LogPath.1" -Force -ErrorAction SilentlyContinue
        }
    } catch {
        # Never abort on rotation failure.
    }
    
    function Write-LauncherLog {
        param([string]$Message)
        try {
            $stamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
            Add-Content -Path $LogPath -Value "[$stamp] $Message" -ErrorAction SilentlyContinue
        } catch {
            # Never abort on log failure.
        }
    }
    
    function Die {
        param([string]$Message)
        Write-LauncherLog "ERROR: $Message"
        [Console]::Error.WriteLine("trace-mcp launcher: $Message")
        [Console]::Error.WriteLine('Recovery: npm i -g trace-mcp && trace-mcp init')
        [Console]::Error.WriteLine('          (or set TRACE_MCP_NODE_OVERRIDE / TRACE_MCP_CLI_OVERRIDE)')
        exit 127
    }
    
    # cli.js is built for the `engines.node` range in package.json. An older node
    # does not fail loudly - it dies on a SyntaxError the MCP client can only report
    # as "failed to connect", which is why the major is checked before we exec.
    # Parsed, not cast: a bare [int] cast of an out-of-range value throws under
    # $ErrorActionPreference = 'Stop' and would abort the launcher outright.
    $NodeMinMajor = 22
    if ($env:TRACE_MCP_NODE_MIN_MAJOR) {
        $parsedMin = 0
        if ([int]::TryParse($env:TRACE_MCP_NODE_MIN_MAJOR, [ref]$parsedMin) -and $parsedMin -gt 0) {
            $NodeMinMajor = $parsedMin
        }
    }
    
    # --- 1. Parse config safely (no Invoke-Expression, whitelist keys) ---
    $NodePath = ''
    $CliPath  = ''
    $UsingOverride = $false
    $UsingNodeOverride = $false
    
    # Every file this shim reads is a hint, never a requirement: launcher.env,
    # pkg-roots, .npmrc. Under $ErrorActionPreference = 'Stop' a read that throws -
    # a file locked by another writer, an I/O error on a mapped drive - escapes the
    # whole launcher, so the client gets neither the recovery message nor the probe
    # fallback and loses trace-mcp for the session. An unreadable hint must degrade
    # to "no hint" (TRA-797).
    function Read-LauncherLines {
        param([string]$Path)
        if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return @() }
        try { return @([System.IO.File]::ReadAllLines($Path)) } catch { return @() }
    }
    
    $configLines = Read-LauncherLines $ConfigPath
    if ($configLines.Count -gt 0) {
        foreach ($line in $configLines) {
            $trimmed = $line.TrimStart()
            if (-not $trimmed -or $trimmed.StartsWith('#')) { continue }
            $idx = $trimmed.IndexOf('=')
            if ($idx -le 0) { continue }
            $key = $trimmed.Substring(0, $idx).Trim()
            $val = $trimmed.Substring($idx + 1).Trim()
            # Strip exactly one pair of surrounding double-quotes if present.
            if ($val.Length -ge 2 -and $val.StartsWith('"') -and $val.EndsWith('"')) {
                $val = $val.Substring(1, $val.Length - 2)
            }
            switch ($key) {
                'TRACE_MCP_NODE' { $NodePath = $val }
                'TRACE_MCP_CLI'  { $CliPath  = $val }
                # TRACE_MCP_NODE_MAJOR and TRACE_MCP_VERSION ignored
                # (informational only; older configs may still carry them)
            }
        }
    }
    
    # --- 2. Env overrides ---
    # $UsingOverride gates persistence (never bake an override into the config);
    # $UsingNodeOverride gates the version check, and only the node override may
    # waive that.
    if ($env:TRACE_MCP_NODE_OVERRIDE) {
        $NodePath = $env:TRACE_MCP_NODE_OVERRIDE
        $UsingOverride = $true
        $UsingNodeOverride = $true
    }
    if ($en
  • hooks/trace-mcp-launcher.shGitHub
    Read the script
    #!/bin/bash
    # trace-mcp-launcher v0.6.16
    # Stable shim: MCP clients invoke this path forever; it resolves node + cli.js
    # at runtime from a config file written by `trace-mcp init`, with a probe
    # fallback for when the config is stale (e.g. Node was reinstalled, or the
    # global package moved to a different npm prefix).
    #
    # Managed by trace-mcp — do not edit by hand. Re-run `trace-mcp init` to refresh.
    
    set -u
    
    # HOME is not guaranteed. A systemd unit with `User=` but no `Environment=HOME`,
    # a container ENTRYPOINT, a bare launchd job, an `env -i` wrapper — all of them
    # spawn the MCP client without it, and every bare `$HOME` below is then a hard
    # abort under `set -u`: exit 1, a raw bash error into the client's stderr, no
    # recovery message, no log line, no probe. The probe loss is the worse half:
    # `node_candidates` expands its whole `for` list before the first iteration, so
    # `/opt/homebrew/bin/node` — listed *before* the `$HOME` entry — is never even
    # emitted, and a machine with a perfectly good node reports "node binary not
    # found" (TRA-1163, reproduced both ways).
    #
    # One guard here rather than `${HOME:-}` at ~15 call sites: the failure is the
    # class, not any one path. Bash expands `~` from the passwd database when HOME
    # is unset, which is exactly the value the missing variable should have had.
    #
    # The `unset` is load-bearing, not tidiness. HOME set to the EMPTY STRING is a
    # real spawn shape (a launcher that exports every variable it knows, known or
    # not), and `~` consults the passwd database ONLY when HOME is unset — with
    # HOME="" it expands to the current value, i.e. stays empty, and every path
    # below resolves against `/`: `$HOME/.trace` becomes `/.trace`, which is
    # unwritable for a normal user and, in a container running as root, is
    # silently CREATED at the filesystem root.
    #
    # If passwd has no home either, refuse to root paths at `/` — a nonexistent
    # directory makes the probe fall through to its npm-prefix sources and, at
    # worst, die with the recovery message, which is the honest outcome.
    #
    # Exported, because cli.js resolves its own state directory from it and an
    # unset HOME there is a second outage hiding behind this one.
    if [ -z "${HOME:-}" ]; then
      unset HOME
      HOME=~
      [ -n "$HOME" ] || HOME=/nonexistent
    fi
    export HOME
    
    # The shim inherits the MCP client's PATH, and a client started in a project
    # directory routinely carries that repository's `node_modules/.bin` on it. Every
    # helper this script runs (date, sed, head, ls, realpath, mv, ...) would
    # otherwise be resolvable to a repo-controlled executable. Pin PATH to system
    # directories for our own work and hand the client's PATH back to node right
    # before exec — the server itself needs it to find git, LSP servers and npm.
    CLIENT_PATH="$PATH"
    PATH=/usr/bin:/bin:/usr/sbin:/sbin
    export PATH
    
    # Determine TRACE_HOME:
    # 1. Explicit TRACE_MCP_HOME or TRACE_MCP_DATA_DIR override always wins.
    # 2. Sibling directory of this shim: the shim is installed at <TRACE_HOME>/bin/trace
    #    or <TRACE_HOME>/bin/trace-mcp. If launcher.env exists in that parent directory,
    #    use it. This survives MCP clients spawned with an isolated or modified HOME
    #    (e.g. Antigravity, containers, launchd, Claude Code --isolated).
    # 3. $HOME/.trace (the standard default).
    # 4. $HOME/.trace-mcp (pre-TRA-611 legacy home).
    if [ -n "${TRACE_MCP_HOME:-}" ]; then
      TRACE_HOME="$TRACE_MCP_HOME"
    elif [ -n "${TRACE_MCP_DATA_DIR:-}" ]; then
      TRACE_HOME="$TRACE_MCP_DATA_DIR"
    else
      SHIM_FILE=""
      if [ -n "${BASH_SOURCE[0]:-}" ]; then
        SHIM_FILE="${BASH_SOURCE[0]}"
      elif [ -n "${0:-}" ]; then
        SHIM_FILE="$0"
      fi
      if [ -n "$SHIM_FILE" ]; then
        REAL_SHIM="$SHIM_FILE"
        # Dereference symlinks so invoking via ~/.trace-mcp/bin/trace-mcp (which symlinks
        # to ~/.trace/bin/trace) resolves to the canonical install directory holding
        # launcher.env even when ~/.trace-mcp has no config of its own.
        while [ -L "$REAL_SHIM" ]; do
          target="$(readlink "$REAL_SHIM" 2>/dev/null)" || break
          case "$target" in
            /*) REAL_SHIM="$target" ;;
            *) REAL_SHIM="$(dirname "$REAL_SHIM")/$target" ;;
          esac
        done
        SHIM_DIR="$(dirname "$REAL_SHIM")"
        if [ -d "$SHIM_DIR" ]; then
          CANDIDATE_HOME="$(cd "$SHIM_DIR/.." 2>/dev/null && pwd -P)"
          if [ -n "$CANDIDATE_HOME" ] && { [ -f "$CANDIDATE_HOME/launcher.env" ] || [ -f "$CANDIDATE_HOME/.config.json" ]; }; then
            TRACE_HOME="$CANDIDATE_HOME"
          fi
        fi
      fi
      if [ -z "${TRACE_HOME:-}" ]; then
        TRACE_HOME="$HOME/.trace"
        if [ ! -d "$TRACE_HOME" ] && [ -d "$HOME/.trace-mcp" ]; then
          TRACE_HOME="$HOME/.trace-mcp"
        fi
      fi
    fi
    
    # Export so cli.js (and src/global.ts) connects to the same state directory
    export TRACE_MCP_HOME="$TRACE_HOME"
    export TRACE_MCP_DATA_DIR="$TRACE_HOME"
    
    CONFIG="$TRACE_HOME/launcher.env"
    LOG="$TRACE_HOME/launcher.log"
    # One global node_modules root per line, appended by each install.
    PKG_ROOTS_FILE="$TRACE_HOME/pkg-roots"
    # Where the desktop app last saw itself, written by the app on every launch
    # (packages/app/src/main/install-path.ts). The only pointer to an app-only
    # install that survives the app being moved.
    APP_LOCATION_FILE="$TRACE_HOME/app-location.json"
    
    # Rotate once per invocation, before the first append (TRA-702). The shim runs
    # once per MCP client launch and writes a couple of lines, so a size check here
    # costs one stat and bounds the file at 2 x LOG_MAX_BYTES across both
    # generations. Without it launcher.log only ever grew — 9.7 MB observed.
    LOG_MAX_BYTES=${TRACE_MCP_LOG_MAX_BYTES:-5242880}
    # The override is user input. `[ x -gt y ]` on a non-numeric operand prints
    # "integer expression expected" straight to the MCP client's stderr, which is
    # exactly the leak TRA-797 closed elsewhere — bound it here, as the shim
    # already does for NODE_MIN_MAJOR below.
    case "$LOG_MAX_BYTES" in
      ''|*[!0-9]*) LOG_MAX_BYTES=5242880 ;;
    esac
    rotate_log() {
      [ -f "$LOG" ] || return 0
      # stat is not portable between GNU and BSD
  • hooks/trace-mcp-mirror.shGitHub
  • hooks/trace-mcp-precompact.shGitHub
  • hooks/trace-mcp-reindex.shGitHub
  • hooks/trace-mcp-session-end.shGitHub
  • hooks/trace-mcp-session-start.shGitHub
  • hooks/trace-mcp-stop.shGitHub
  • hooks/trace-mcp-user-prompt-submit.shGitHub
  • hooks/trace-mcp-worktree.shGitHub

All 14 scripts are listed above. The source is inlined for 6 of them, starting with whatever hooks.json actually runs. See all of them in the repo.

Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.

@nikolai-vysotskyi
Ships withtrace-mcp

Framework-aware code intelligence MCP server — 91 framework integrations, 81 languages, 72.7% fewer input tokens to review a pull request, comprehension at parity

Stars on GitHub

Momentum

stars in total
203
Star history of nikolai-vysotskyi/trace-mcp on GitHub, a chart by star-history.com

More plugins that ship hooks.

superpowers

superpowers

A development methodology for coding agents that makes the agent brainstorm before building, follow plans, run parallel work and finish branches in a disciplined way. You get fewer skipped steps and more consistent results from your coding assistant.

15 skills1 hook311k
@obra@obra
Install Plugin
ponytail

ponytail

A Claude Code plugin with skills for auditing a whole repo, reviewing a change, listing deferred shortcut comments, and showing measured savings and a quick command reference.

6 skills10 hooks188k
@dietrichgebert@dietrichgebert
Install Plugin
agent-skills

agent-skills

Walks a software change from a written spec through design, testing, code review, CI/CD, and release, applying the matching engineering skill at each stage. Useful for developers using Claude Code on full development workflows.

25 skills+3112k
@addyosmani@addyosmani
Install Plugin
planning-with-files

planning-with-files

Persistent file-based planning for AI coding agents on long-running, multi-step tasks. It keeps task_plan.md, findings.md and progress.md on disk, so work can resume after /clear or context compaction.

6 skills+228k
@othmanadi@othmanadi
Install Plugin
gsd-core

gsd-core

A spec-driven development workflow for Claude Code that guides a project through planning, phases, milestone audits and fixes. Includes skills to generate tests from UAT criteria and to audit completed work.

72 skills+311k
@open-gsd@open-gsd
Install Plugin
superpowers-zh

superpowers-zh

Chinese-language skill pack for Claude Code that adds workflow guidance for brainstorming, TDD, debugging, and code review, plus Chinese conventions for commits, documentation, and domestic Git platforms like Gitee and Coding.net.

21 skills1 hook8.5k
@jnmetacode@jnmetacode
Install Plugin
socraticode

socraticode

Gives Claude Code semantic search, dependency graph and architecture context over a codebase, so an AI assistant can explore and understand a project's code. Includes skills for indexing a project and managing its file watching.

2 skills+33.4k
@giancarloerra@giancarloerra
Install Plugin
cocoindex-code

cocoindex-code

A lightweight code search engine that uses AST-based semantic search with local embeddings and incremental indexing. It helps Claude Code users find code faster and more efficiently.

1 skill1 MCP+13k
@cocoindex-io@cocoindex-io
Install Plugin

Questions about the hooks in trace-mcp.

Which coding agents can use the hooks in trace-mcp?
The hooks in trace-mcp install in Claude Code and Cursor: each runs Claude Code hooks from .claude/settings.json, so copy the hooks there.