A Claude Code plugin that refines your prompts as you submit them.
Why Refract over other prompt-improver hooks: every failure fails open (your prompt always proceeds untouched, exit 0, no exceptions), zero runtime dependencies, no API key required (it rides Claude Code's own login), every decision is one auditable line in a local log, and injected refinements carry explicit precedence framing — your original wording always wins. Refinements are grounded in what is actually true: the session transcript, your profile, and the repository's live git state. A UserPromptSubmit hook decides — deterministically, in-process — whether a prompt is worth refining. Trivial turns (acknowledgments, option picks, slash commands, code pastes, bare paths/URLs) skip instantly with zero latency and zero API cost. Substantive but underspecified prompts get a structured expansion injected alongside your original wording; genuinely ambiguous ones get suggested clarifying questions instead.
Your prompt is never removed or altered. Injected text carries explicit precedence framing: your original wording wins wherever the two conflict. Every decision is one line in a local audit log, and every failure fails open — on any error, timeout, or missing credential the prompt proceeds untouched and the hook exits 0.
- Works: triage, both engines (CLI and direct API), expansion and clarify injection, the audit log,
/refract:refine,/refract:profile,/refract:stats, the eval harness, per-project.refract.md. 69 offline tests cover all of it. - Rough: the CLI engine pays one to three seconds of
claudeprocess startup per engaged prompt; Windows setups usingnvm/fnmoften hidenodefrom non-interactive shells (see Requirements). - Planned: an opt-in rewrite mode, blocked on Claude Code supporting
updatedPromptinUserPromptSubmithooks (anthropics/claude-code#27365).
- Node.js 18 or newer on the
PATHseen by non-interactive shells. Claude Code launches the hook as a barenodecommand. Version managers such asnvmandfnmon Windows commonly makenodeavailable only in interactive shells, leaving it off thePATHnon-interactive processes see — this is the most common installation failure. Verify withnode --versionin a plaincmd.exewindow (not your configured terminal profile). - No API key required. By default Refract reuses the authentication Claude Code already has.
/plugin marketplace add TevBenji/refract
/plugin install refract@refract
Or clone this repo and add the local directory as a marketplace. No build step is needed — hooks/refract.mjs is a committed, zero-dependency bundle.
Refract makes one small model call per engaged prompt. The engine key in config.json picks how:
"auto"(default) — no configuration at all: Refract shells out toclaude -p(the CLI's print mode), which runs on whatever authentication Claude Code itself is using — subscription OAuth login, an API key, Amazon Bedrock, or Google Vertex. Subscription users see the call counted against their plan usage like any other turn. If you have deliberately setREFRACT_API_KEY, auto switches to the direct API engine below."cli"— always useclaude -p, even when a key is set."api"— callhttps://api.anthropic.com/v1/messagesdirectly with an API key. This shaves the CLI's process-startup latency (roughly one to three seconds per engaged call) and bills the key instead of your plan.
In api mode Refract reads REFRACT_API_KEY first; when that is absent or empty it falls back to ANTHROPIC_API_KEY. Prefer the dedicated variable: Claude Code reads ANTHROPIC_API_KEY for its own billing path, so a machine-wide export you intended for Refract can be silently consumed by Claude Code itself. REFRACT_API_KEY keeps the two separate.
Windows (PowerShell, persistent for your user):
[Environment]::SetEnvironmentVariable('REFRACT_API_KEY', 'sk-ant-...', 'User')or cmd.exe:
setx REFRACT_API_KEY sk-ant-...
Restart Claude Code afterwards so the new environment is picked up. If engine is "api" and neither variable is set, every prompt passes through untouched and the audit log records no_api_key. In the default auto mode that situation cannot arise — with no key present, the CLI engine is used.
Only on engaged prompts, and only to your configured Claude provider — https://api.anthropic.com in api mode and for CLI users authenticated with Anthropic; your own Bedrock or Vertex endpoint when Claude Code is configured for those:
- the prompt text
- a capped excerpt of the recent session transcript (default at most 12,000 characters)
- the contents of your
profile.md(plus.refract.mdwhen the project has one) - the working directory path
- the repository state: current branch and
git status --shortoutput, capped at 1,500 characters (empty outside a git repo)
Nothing else is sent, and no other endpoint is ever contacted. Triage-skipped prompts make no network request at all. Keep secrets out of profile.md.
%CLAUDE_PLUGIN_DATA% when Claude Code sets it, otherwise ~/.claude/refract/ (on Windows: C:\Users\<you>\.claude\refract\):
| File | Purpose |
|---|---|
profile.md |
Your stack, conventions, and definition of done — seeded with a neutral template on first run; edit it |
config.json |
Settings — seeded with defaults on first run |
log.jsonl |
One JSON line per prompt submission: verdict, triage rule, latency, token counts, warnings |
log.1.jsonl |
Single rotated generation, produced when log.jsonl exceeds logMaxMB |
This directory survives plugin updates; nothing is ever written inside the plugin's install directory.
Prefix any prompt with raw: (configurable via bypassPrefix in config.json) to skip refinement for that one prompt:
raw: just tell me a joke
Prefix with refine: (configurable via forcePrefix) to force refinement even on prompts triage would normally skip — long pastes, symbol-dense text, mostly-fenced content:
refine: <a 10,000-character stack trace> — what should I actually be asking here?
Matching is case-insensitive and both prefixes are left in your prompt text.
Setting the environment variable REFRACT_SKIP=1 disables the hook entirely for that process — Refract sets it on its own inner CLI session as a recursion guard, and you can set it yourself to switch the hook off for a whole Claude Code session.
Drop a .refract.md file in a project's root and Refract appends it (capped at 4 KB) to your profile for prompts submitted from that directory. Commit it to share per-repo conventions — stack, style, definition of done — with everyone using the plugin. Your global profile.md still applies everywhere.
An engaged call runs roughly 4,000–5,000 input tokens at the default contextCharBudget of 12,000 characters (meta prompt + context excerpt + profile + prompt), plus up to 1,000 output tokens, on the configured model. In the default CLI engine this counts against your Claude plan usage (or your configured provider); in api mode it bills the key. The input_tokens and output_tokens fields in log.jsonl are the meter — read a few hundred real records before tuning contextCharBudget, maxTranscriptTurns, or perMessageCharCap, rather than guessing.
Latency: the CLI engine pays claude process startup (roughly one to three seconds) on top of the model call; the api engine is a single HTTPS round trip. Both are bounded by selfTimeoutMs and fail open to an untouched prompt on expiry.
To hard-cap spend, set maxEngagedPerDay in config.json: once that many engaged calls have run in the current UTC day, further prompts pass through untouched and the log records budget_exceeded. The default 0 means unlimited.
The default model is the claude-haiku-4-5 alias, which follows Anthropic's latest Haiku snapshot and picks up improvements without action. For behavior that stays fixed until you move it, set the dated snapshot in config.json:
{ "model": "claude-haiku-4-5-20251001" }Refract's audit log doubles as a regression set for the meta prompt. Set logFullPrompt: true in config.json to store full prompt text (locally only), collect some real usage, then:
node scripts/eval.mjs # replay logged prompts, compare verdicts to the log
node scripts/eval.mjs --meta variant.txt # try a candidate meta prompt against the same prompts
Each replay makes one model call per prompt on your configured engine, prints the verdict distribution, and flags every prompt whose verdict changed. Never ship a meta-prompt change that flips verdicts you liked. Without logFullPrompt the harness falls back to the 200-character prompt_head (marked [head] in the output), which is a weaker signal.
UserPromptSubmit hooks currently cannot replace prompt text, only add context alongside it — which is why Refract's precedence-framed injection design exists. If anthropics/claude-code#27365 (updatedPrompt support) lands, an opt-in rewrite mode becomes possible.
Injected expansions become part of the session transcript, so claude --resume replays previously injected context along with the rest of the session. This is harmless — the precedence framing still applies — but expect to see past expansions again in resumed sessions.
/refract:refine <draft prompt>— asks the main Claude model (not the hook) to produce a structured spec — objective, scope, constraints, definition of done, open questions — for your review before anything runs./refract:profile [instruction]— show your stored profile; with no argument it reviews it and suggests improvements grounded in the current project, with an instruction it edits the file and writes it back./refract:stats— summarize the local audit log: skip/engage totals, verdict breakdown, latency, token spend, and the last few decisions.
Slash commands are triage-skipped, so none of these cost anything at the hook layer.
| Key | Default | Meaning |
|---|---|---|
enabled |
true |
false disables the plugin entirely (instant pass-through) |
engine |
"auto" |
auto = claude -p on your existing login, or direct API when REFRACT_API_KEY is set; cli / api force one |
notify |
"refined" |
Visible status line per decision: refined = only when something was injected, all = every prompt (including skips), off = never |
model |
"claude-haiku-4-5" |
Rewriter model |
maxTranscriptTurns |
10 |
Max user/assistant messages of session context |
perMessageCharCap |
1200 |
Per-message truncation (assistant: tail, user: head) |
contextCharBudget |
12000 |
Total context excerpt cap |
selfTimeoutMs |
10000 |
Self-imposed rewriter timeout; clamped to 10,000 max |
maxRewriteWords |
250 |
Rewrite length cap (sentence-boundary truncation) |
bypassPrefix |
"raw:" |
Per-prompt bypass prefix |
forcePrefix |
"refine:" |
Per-prompt force-refinement prefix (overrides triage skips) |
maxEngagedPerDay |
0 |
Hard cap on engaged calls per UTC day; 0 = unlimited |
logFullPrompt |
false |
Store the full prompt text (not just a 200-char head) in each audit record, locally, for the eval harness |
logMaxMB |
20 |
Log rotation threshold |
Keys absent from config.json take these defaults.
npm install
npm test # builds hooks/refract.mjs, then runs the offline checks (node --test)
npm run typecheck
All checks are offline (injected fetch, temporary stores under the OS temp dir) and use the built-in node:test runner. Dev dependencies only; the shipped bundle has zero runtime dependencies.
Manual integration checks, not part of the offline suite:
- launch the hook from
cmd.exe, PowerShell, and bash with a small JSON payload on stdin and confirm exit code 0; - triage-skip end-to-end wall time under 500 ms (Node process startup dominates; Windows real-time antivirus can consume 150–300 ms of that alone);
- engaged wall time under
selfTimeoutMsplus startup overhead; - kill the process mid-flight and confirm no
log.jsonlline was written and the prompt proceeded unmodified.