/** * The `PreToolUse(Agent)` hook that makes `offload on` mean something on a host whose traffic * never reaches this relay. * * ## Why a hook at all * * `routing.subagents` reroutes a subagent by answering its HTTP request differently. That requires * the request to arrive. In a Claude Desktop session it does not (see `src/host-routing.ts`), so * the switch reports ON and nothing changes — the failure this exists to close. * * ## What a hook can and cannot do — verified against the hooks reference, 2026-08-07 * * It CANNOT redirect the subagent. The subagent is served by the same Claude Code process over the * same connection with the same pinned base URL, so no hook output changes where its tokens are * spent. `PreToolUse` can rewrite tool arguments via `updatedInput`, but rewriting the prompt to * carry an `@relay:` directive just sends that line to Anthropic as literal text, and rewriting * `model` picks a different Anthropic model. `SubagentStart` is context-only by specification — * no blocking, no model or endpoint control. * * So this is a FORCING FUNCTION, not a redirect: it denies the `Agent` call and hands back the * command that does reach the offload target. The deny is enforced; that the agent then runs the * command is not. Describe it that way — overselling it is how a nudge gets mistaken for a * guarantee and quota gets spent believing otherwise. * * ## Fail open, always * * Every failure path in the generated script allows the call. A hook that denied subagents because * the proxy was down would convert one unavailable optional lane into "no subagent works at all" — * the same reasoning that makes an unset `${ENV}` disable one provider instead of aborting startup. */ /** Marker filename. Identifies OUR entry in a settings file that may hold the user's own hooks. */ export declare const AGENT_HOOK_FILENAME = "llm-relay-agent-offload.mjs"; export interface ClaudeHookPaths { /** `~/.claude/settings.json` — the harness config this appends to. */ settings: string; /** Where the generated hook script lives. Under the relay's own directory, not the harness's. */ script: string; } export declare function claudeHookPaths(home?: string): ClaudeHookPaths; /** * The hook script, generated rather than shipped as a package file so it carries this install's * own interpreter and CLI path with no runtime resolution. * * ⚠ It shells out to `llm-relay dispatch --next-command` instead of talking to `/dispatch` and * rendering the command itself. That costs a process start per Agent call (not a hot path) and * buys the one thing worth paying for: a single implementation of the shell-quoting rules. A * second copy here would be the copy that drifts, and a drifted quoter renders a command line * whose task text can break out into extra shell words. * * ⚠ It runs ` `, never the installed launcher. On Windows an npm global install's * `llm-relay` IS a `.cmd`, and `execFileSync` refuses to execute `.cmd`/`.bat` without a shell * (blocked since the CVE-2024-27980 fix); a bare `.js` path is not executable either. Both fail * the same silent way — the hook's own fail-open catch swallows the error and every Agent call is * allowed, so the feature looks installed and does nothing. Naming the interpreter explicitly * avoids the shell entirely, which is also the safer construction. */ export declare function renderAgentHookScript(nodeBin: string, cliScript: string): string; export interface HookChange { changed: boolean; settingsPath: string; scriptPath?: string; } /** * Install (or refresh) the hook. * * ⚠ APPENDS a new matcher entry; it never rewrites an existing one. Users legitimately run their * own `Agent` hooks — this machine has one enforcing an explicit subagent model — and Claude Code * runs every matching entry, so coexistence is the correct shape. Replacing the array, or editing * a matching entry in place, would silently delete somebody's policy. */ export declare function installAgentHook(nodeBin: string, cliScript: string, paths?: ClaudeHookPaths): HookChange; /** * Remove the hook wiring. The generated script is deliberately LEFT on disk: it is inert without * the settings entry, it costs nothing, and deleting files outside this tool's own directory on a * toggle is a bigger action than the toggle asked for. */ export declare function removeAgentHook(paths?: ClaudeHookPaths): HookChange; /** Is our hook currently wired into the harness? */ export declare function agentHookInstalled(paths?: ClaudeHookPaths): boolean;