Is a Claude Code routing hook safe? What Downshift reads and changes

Downshift by Tiago Vilas Boas. Updated

Downshift here is a Go tool for AI coding agents, not the React downshift autocomplete library and unrelated to any car transmission.

TL;DR

A routing hook runs as a local command that Claude Code calls before it spawns a subagent. Downshift is built so that call is small and boring: no network and no model call to decide, two fields rewritten (model and reasoning_effort), every other field preserved, every failure leaves your spawn unchanged, and task text never reaches disk. Each claim below links to the file in the repository that states it. The last section lists what Downshift does not do.

What can a Claude Code hook do to my session?

A PreToolUse hook is a command Claude Code runs before a tool call. Anthropic documents that it can allow, block, or replace the tool input through updatedInput. It runs with your user permissions, so the question is not whether hooks are powerful. It is what this specific hook is allowed to touch. Anthropic also documents how failures behave: exit 2 blocks, any other non-zero exit or a timeout does not block unless a hook sets onFailure: "block". Checked on 2026-10-24

Downshift registers on the Agent|Task matcher, so it sees subagent spawns, not every tool call. That scope is set by downshift setup, described in docs/guides/install.md.

What does Downshift read, and where does it run?

It reads the hook payload Claude Code sends on stdin, parses it strictly, and answers on stdout. The whole decision is local and deterministic: a scored signal table, a hash embedder and a naive-Bayes opinion, with no network call and no LLM in the loop. The design doc states the hot path has "no network, no LLM, no prompt storage" (docs/design/architecture.md). It is not an HTTP proxy: nothing sits between Claude Code and the model API.

The code is open source under Apache-2.0 (LICENSE) and the Go binary has zero external Go dependencies (SECURITY.md).

What exactly does the hook change in a spawn?

Only two fields. When a rewrite is justified, the response is an updatedInput in which model and, where the harness takes it, reasoning_effort change; every sibling field is preserved. This is a stated invariant in AGENTS.md and in the flow of docs/design/architecture.md, and the shared conformance suite pins the unchanged-spawn answer: an unchanged Claude Code spawn answers {}, not an explicit allow, so your own ask rules still apply (docs/reference/harness-matrix.md).

It does not edit tool results either: the PostToolUse hook returns {} (plus an optional status line).

What happens if the hook crashes, hangs or gets a huge payload?

Your spawn goes through unchanged. Downshift reads at most 1 MiB from stdin, with a 2 s read timeout, runs under a 1.5 s hook deadline, recovers panics into an allow, and loads its catalog under recover. A failure never exits with code 2, which is the code that blocks in Claude Code. See the "Trust boundaries" table in docs/design/architecture.md. The same file records the cost of this choice: a failed hook means a missed optimization, never a blocked task.

Does Downshift store my prompts, task text or file paths?

No. Task text stays in memory for feature extraction. The event log holds metadata only: model names, complexity classes, guardrail triggers, numeric token counts and hashed identifiers, plus timings. Prompt text, file paths and tool outputs never reach disk (docs/domains/observability-telemetry.md). The shareable export is counts only: downshift stats --days=30 --export, described in docs/reference/stats-export.md; even a custom agent name is never stored. You can inspect the local log yourself; it is a plain JSON Lines file under ~/.downshift.

Can it send risky work to a smaller model?

The design says no. The classifier has risk categories (secrets and credentials, crypto, payments, destructive operations such as DROP TABLE, rm -rf or force-push) that set a floor, so a task touching them does not get a smaller model (internal/classify/risk.go). Guardrails R1 to R9 then run on the result, and any violation turns the verdict into "keep the current model". Under doubt, an unconfident downshift, upshift or frontier pick is held at your model (docs/domains/subagent-routing.md). About 10% of spawns also pass unchanged as a control group, so you can measure the effect.

This is a classifier made of patterns, not a proof. A risky task described in words the patterns do not cover can look routine. If you need a hard guarantee for a folder, set a project tier floor in .downshift/policy.json.

What does the output compactor rewrite in my shell commands?

A separate feature, downshift compact, wraps allowlisted test and build commands (for example go test, pytest, vitest) to cut their output to the summary. The rules in docs/reference/harness-matrix.md: the command must start with an allowlisted family and contain no shell metacharacter, except a short list of known shapes (cd dir && cmd, | tail -N, and chains split at ; and &&). The real exit code is kept. A deny or ask rule that matches the command or any segment stops the rewrite, and "exec": false or DOWNSHIFT_EXEC=0 turns it off. Warnings, errors and ambiguous output are kept byte for byte; only complete, internally consistent passing summaries are compacted (docs/guides/benchmark-output-cutting.md).

Measured on synthetic fixtures (corpus of 2026-10-21): 82.3% fewer bytes on the 26 of 248 files that compact, 23.7% across all 248. Those are bytes, not LLM tokens.

How do I install it, check it, and undo it?

Three controls, all in docs/guides/install.md:

  • Verify the download. The installer checks the release archive against checksums.txt (sha256) and stops on a mismatch. Releases also carry a GitHub build-provenance attestation; with the gh CLI present it runs gh attestation verify, and by hand you can run it yourself: gh attestation verify downshift_<ver>_<os>_<arch>.tar.gz --repo tiagovilasboas/downshift. Without gh it falls back to sha256 only and says so.
  • Preview before writing. downshift setup --dry-run prints the diff for each harness found and writes nothing. Setup keeps unrelated keys and existing hooks, and saves a timestamped backup.
  • Undo. downshift setup --undo restores the latest backup. downshift doctor checks the install.

What does Downshift not do? Known limits

  • It does not guarantee the rewrite. The harness executes the child and may ignore updatedInput. Per docs/reference/harness-matrix.md: honored and observed on Claude Code (2026-10-06); inferred on Codex; unconfirmed on Cursor, where it is dropped on Free and legacy Pro; KiroCrew has no in-place rewrite and gets a block-with-reason and retry; Grok is config only; OpenCode is not observed yet.
  • It cannot see or edit the subagent. It does not run the child, cannot see its context and never edits what it returns.
  • The Compactor skips what it cannot prove safe. It leaves ||, a lone &, pipes into anything but tail or head -N, file redirects, heredocs, subshells, quotes, globs and expansions unwrapped, and refuses a whole chain when one segment holds one of these.
  • Risk detection is pattern based. See above.
  • Savings are not guaranteed. See the status note; the interval of the routing result starts close to zero.
  • It is not a security product. It is a cost-routing hook that avoids widening your attack surface. Vulnerabilities go to GitHub Private Vulnerability Reporting (SECURITY.md).

Frequently asked questions

Is a Claude Code hook safe to install?

A hook runs a local command with your permissions, so check what it does. Downshift runs locally with no network or model call to decide, rewrites only model and reasoning_effort, preserves every other field, and leaves the spawn unchanged on any failure. You can preview the install with downshift setup --dry-run and undo it with downshift setup --undo.

Does the hook see my code or prompts?

It receives the hook payload for a subagent spawn and reads the task text in memory to classify it. The task text is never persisted. Local telemetry holds metadata only: counts, hashes and timings, no prompts, task text or file paths.

What happens if the hook breaks?

It fails open. Stdin is capped at 1 MiB with a 2 s read timeout and a 1.5 s deadline, a panic becomes an unchanged spawn, and a failure never exits with the blocking code 2. The cost of a failure is a missed optimization, not a blocked task.

Can a router send secrets, auth or payment code to a cheap model?

Downshift sets a floor for those risk areas (secrets, auth, crypto, payments, destructive operations), so they do not get a smaller model, and under doubt your model is kept. Detection is pattern based, not a proof; use a project tier floor for a hard guarantee.

How do I verify the Downshift download?

The installer checks the sha256 checksum from the release. Releases also carry a build-provenance attestation you can verify with gh attestation verify against tiagovilasboas/downshift.

Install

Run downshift setup --dry-run first to preview every change. downshift setup --undo restores your backups. Full steps: /docs/install/.

curl -fsSL https://raw.githubusercontent.com/tiagovilasboas/downshift/main/install.sh | sh && downshift setup

Claude Code plugin

/plugin install downshift --marketplace tiagovilasboas/downshift

Sources

  1. Anthropic docsdocumented
    code.claude.com/docs/en/hooks
    PreToolUse decision control (updatedInput), exit-code and timeout behaviour, onFailure (fetched 2026-10-24)
  2. Downshift repomeasured
    AGENTS.md
    key invariants
  3. Downshift repomeasured
    docs/design/architecture.md
    end-to-end flow, trust boundaries, what Downshift is not
  4. Downshift repomeasured
    docs/reference/harness-matrix.md
    rewrite-honored matrix, Compactor rules and chains
  5. Downshift repomeasured
    docs/guides/install.md
    checksums, attestation, setup --dry-run and --undo
  6. Downshift repomeasured
    docs/reference/stats-export.md
    counts-only export
  7. Downshift repomeasured
    SECURITY.md
    scope, reporting, zero dependencies
  8. Downshift repomeasured
    docs/evidence/four-arm-mechanical-2026-10-21.md
    46-run battery and interval