vigiles API reference
    Preparing search index...

    Function pathView

    vigiles/hook — the closed vocabulary for authoring a compiled hook.

    A hook today is opaque shell (bash guard.sh): un-analyzable, and the author hand-writes the fragile parts (exit code, JSON field, a grep matcher) that cause the #1 verified hook pain — FALSE CONFIDENCE (a guardrail that looks like it blocks but silently doesn't). Invert it: author a pure typed function (event) => Decision against THIS surface, and vigiles compile emits the harness protocol for you. The whole false-confidence class becomes UNREPRESENTABLE — you never write the exit code or the field.

    The roles, each with its own output type so a category mistake is a tsc error, not a silent no-op:

    • experimental_defineHook / experimental_defineFileGate — a gate returns a Decision (allow/deny/ask); deny is the only thing that blocks.
    • experimental_definePromptGate — a prompt gate (UserPromptSubmit) sees the prompt TEXT and may deny to block it (a security filter).
    • experimental_defineStopGate — a stop gate (Stop/SubagentStop) may deny to keep the agent going (gate-until-tests-pass).
    • experimental_defineInject — an inject returns an Injection (context text); it has no deny, so "block on a SessionStart hook" won't compile.
    • experimental_defineReact — a react (PostToolUse) returns a Reaction; it sees the tool RESPONSE, its run(cmd) is effect-classified at construction, and it can't block (the tool already ran).

    Every gate takes a mode: enforce (default, blocks) or observe (the shadow/rollout mode — records what it WOULD block, never blocks). observe is harness-neutral (it just exits 0 + writes a local record).

    Matching is AST-backed (command.runs("git push", { force })), so it catches cd x && git push -f that the native Bash(git:*) glob misses.

    A gate may also decide on EXTERNAL STATE by declaring needs (e.g. needs: ["git.branch"]): the trusted runtime gathers those read-only facts and hands them in as e.ctx — the hook still does zero I/O, and reading an undeclared fact is a tsc error. Built-ins: git.branch/git.isDirty/git.root/cwd/os.platform/env.isCI. For a one-off off-catalog fact, the lightweight opt-out is an inline provide(name, cmd) (read-only) or dangerously(name, cmd) (the loud escape) right in needs. See research/hook-context-providers.md.

    ⚠️ Honest scope: compile/verify fix the hook's AUTHORING + LOGIC, not the harness's DELIVERY. The delivery floor MOVED — #34692 (a subagent's tool calls never reaching PreToolUse) is FIXED as of Claude Code 2.1.241, measured against a stock registry install and pinned by src/subagent-delivery.test.ts, which goes red if it regresses. SCOPE of that measurement: headless claude -p only — interactive is unmeasured, and depth-2 subagent nesting does not occur there at all. What has NOT changed: a model can still route around a tool entirely (#45427 / #32376 — a Bash heredoc instead of Write), so a gate is a strong default and is NEVER an unbypassable wall. See docs/compiled-hooks.md.

    • A view of a file path, matched against prefixes by PathView.under.

      🔴 root IS NOT OPTIONAL IN PRACTICE — WITHOUT IT THIS IS DEAD FOR EVERY REAL EVENT. Claude Code's Edit/Write/MultiEdit tools always send an ABSOLUTE file_path, and every hook in the wild writes repo-relative prefixes (under(["drafts/papers/"])). Before the root existed, the two could never meet: the comparison ran "/home/u/repo/drafts/papers/x.tex" against "drafts/papers" and returned false for all input, forever. MEASURED 2026-08-12 on the live runtime with one compiled react hook and two spellings of the same file — the relative one printed its checklist, the absolute one exited 0 in silence. Three shipped hooks in a consumer repo were dead by it, all three compiled specifically to replace hooks that were dead, and none of the tests noticed because the harness helper built relative paths.

      under is the ALLOWLIST/COVERAGE primitive: its callers spell under(P) ? allow() : deny() (confinement) and under(P) ? notice() : nothing() (a nudge). For both, an unprovable answer must be false — a confinement gate denies (fails closed) and a nudge stays quiet. So:

      • path resolves OUTSIDE the root → false for every relative prefix. Not a bias, the truth: a repo-relative prefix names nothing in another checkout. This is the resolveRef lesson — the suffix match it replaced handed /home/other-project/package.json a grant meant for this repo's.
      • NO root known and the path is absolute → false for every relative prefix. Here we genuinely cannot tell, and false is the choice: silence, never a false grant. It is also the pre-fix behaviour, so nothing regresses.

      ⚠️ THE ASYMMETRY INVERTS FOR A DENYLIST, AND under DOES NOT SERVE THAT CASE. A gate written under(["secrets"]) ? deny() : allow() reads an unprovable false as ALLOW — a false grant. The Bash-side counterpart CommandView.touches exists precisely for that direction and takes the opposite bias from the SAME rule (prefixVerdict), which is why the resolver matchesPrefix makes each caller name its own. PathView has no such counterpart; a file-side denylist inherits the allowlist bias. Reported, not papered over.

      Parameters

      • raw: string

        the file_path the tool event carried, any spelling.

      • Optionalroot: string

        the project root relative prefixes resolve against. Comes from the runtime ($CLAUDE_PROJECT_DIR, else the event's own cwd), never from process.cwd() — under a git worktree the process's cwd is a DIFFERENT checkout from the one the harness resolved the hook out of, and that exact mismatch already wedged a repo once.

      Returns PathView