README.md

September 18, 2026 · View on GitHub

Jev (Prompt Coach) - let Jev from TypeSafe AI be your prompt coach

CI Node 22 or newer MIT licence

English · Français · Español

Scores how well you write prompts to a coding agent, and shows whether your habits are improving. Runs on TypeSafe's Jev model.

It adds nothing to the time between pressing Enter and getting a response.

Unofficial community plugin. Not affiliated with, endorsed by, or supported by TypeSafe or Anthropic. You bring your own TypeSafe API key.


Why this exists

Most prompt-quality tools put a language model between you and your agent. They score the prompt before it is sent, which means a round trip on every message and timeouts measured in minutes.

Jev (Prompt Coach) does not sit there. In its default mode the hook appends one line to a local file and exits. Scoring happens when you ask for it, in a command.

The second thing it does differently: it will score the prompts you have already written. A backfill over a year of local Claude Code history costs about six cents, because Jev charges $0.042 per million input tokens and nothing for output. You get a report on day one instead of in two weeks.

Requirements

  • Node 22 or newernode --version
  • Claude Code 2.1.x or newer. Plugin-declared UserPromptSubmit hooks did not execute on some earlier versions, and the plugin depends on them.
  • A TypeSafe API key, from console.typesafe.ai.

The plugin bundles its own dependencies into dist/. There is no install step, no node_modules, nothing fetched at runtime, and the only host it ever contacts is api.typesafe.ai. Installed, it is 376 KB.

Claude Code itself ships as a native binary and brings no Node of its own, so the Node on your PATH is what runs the hook. Node 22 is the floor, and CI tests 22 and 24.

Install

1. Add the marketplace and install the plugin.

claude plugin marketplace add CrowdLinker/JevPromptCoach
claude plugin install jevpromptcoach@jevpromptcoach

Confirm it loaded — status should be enabled:

claude plugin list

2. Give it your API key.

Create the key file first, lock it down, and only then put the key in it — so the key never exists in a world-readable file, and never appears on a command line where your shell would record it in history:

mkdir -p ~/.claude/jevpromptcoach
touch ~/.claude/jevpromptcoach/.env
chmod 600 ~/.claude/jevpromptcoach/.env

Then open ~/.claude/jevpromptcoach/.env in your editor and add one line:

TYPESAFE_API_KEY=your-key-here

The key file is where the plugin looks. A hook does not run under your shell profile, so a key exported only in .zshrc may never reach it, and always mode needs it here. The plugin never writes this file, never logs the key, and never lets it into an error message.

If you would rather not use an editor, this reads the key without echoing it and without putting it in your history:

mkdir -p ~/.claude/jevpromptcoach && touch ~/.claude/jevpromptcoach/.env && chmod 600 ~/.claude/jevpromptcoach/.env
read -rs KEY && printf 'TYPESAFE_API_KEY=%s\n' "$KEY" > ~/.claude/jevpromptcoach/.env && unset KEY

Paste the key at the blank prompt and press Enter. That works in both bash and zsh. TYPESAFE_API_KEY in the environment still takes precedence if you have a reason to set it — that is how CI and the eval supply it — but the file is the one to use day to day.

3. Check it is working.

/jevpromptcoach:config

That prints your mode, privacy level, whether the key was found, and how many prompts have been logged so far. Submit a prompt or two and run it again — if the logged count is not rising, the hook is not firing, and docs/HOOK-BEHAVIOUR.md covers why that happens.

Setup

Pick a mode. The default is on-demand, which never adds latency. Switch only if you want a score on every message:

/jevpromptcoach:config mode always

Pick a privacy level. The default is redact. If prompts in your work should never leave the machine at all:

/jevpromptcoach:config privacy metadata_only

Backfill your history. This is the part worth doing on day one — it scores the prompts you have already written, so the first report covers months instead of nothing:

/jevpromptcoach:config backfill

It prints how many prompts it found and what they will cost, and sends nothing until you confirm. Over 1,039 prompts it cost $0.06.

Then:

/jevpromptcoach:report

If the marketplace refuses to add. A message about the source differing from "the one declared for it in settings" means the name jevpromptcoach is already registered against a different source — most often a local directory, from developing the plugin. Marketplace names are unique, so the GitHub source cannot be added under a name that is taken. Remove the old registration and retry:

claude plugin marketplace remove jevpromptcoach
claude plugin marketplace add CrowdLinker/JevPromptCoach

Removing a marketplace uninstalls the plugins that came from it. Your log, scores and config live in ~/.claude/jevpromptcoach/ and are untouched.

Uninstalling. claude plugin uninstall jevpromptcoach@jevpromptcoach removes the plugin but leaves your data. To delete that too, remove ~/.claude/jevpromptcoach/ — it holds the log, the score cache, your config and the key file, and nothing else.

Commands

Plugin commands are namespaced, and the prefix is not reliably optional — an agent launched via Task or @mention cannot resolve the short form. Always write the full name.

/jevpromptcoach:score <text>

Scores a draft before you send it. This is the teaching surface: it prints the score, every check as pass/fail/not-applicable, and for each failure the cause, the consequence, and the fix — then rewrites your text so it would pass.

/jevpromptcoach:score fix the bug in the code, it doesnt work, refactor everything while youre in there
# Prompt score: 0/100

FAIL  Mentions which file or function  (0.03)
FAIL  States what "done" looks like  (0.26)
FAIL  Keeps to one requirement  (0.06)
...

### Mentions which file or function
- What is missing: You wrote "it" or "the code" instead of a name.
- What goes wrong: The agent has to guess which file you meant. It searches,
  or it edits the wrong one.
- Do this instead: Name the file, function, or symbol you want changed.

With no argument it explains itself and shows an example. It does not error.

/jevpromptcoach:report [N]

Patterns across the last N logged prompts, default 200. Hit rate per check, a 30-day trend, and one habit to work on. Not seven.

See docs/EXAMPLE-REPORT.md for a real one, generated over 1,040 prompts of actual history.

/jevpromptcoach:config

Mode, privacy level, backfill, and clearing the log.

/jevpromptcoach:config                      show current settings
/jevpromptcoach:config mode always          score inline as you type
/jevpromptcoach:config privacy metadata_only
/jevpromptcoach:config backfill             estimate, then confirm
/jevpromptcoach:config clear                delete the local log

The seven checks

Coding-agent habits, not generic prompt engineering. All seven ride in one Jev request per prompt — Jev reads the prompt once and answers every question against it in parallel, so the full set costs about what one question costs.

CheckWhat it looks for
Mentions which file or functionA real name, not "the code" or "it"
States what "done" looks likeWhat should be true when the work is finished
Keeps to one requirementOne concrete change, not several bundled into one message
States what must not changeAnything that must stay as it is
Gives the actual errorThe real error text, or what you expected versus what happened
Asks for a plan firstAsked to see the approach before a big or risky change
States the verification stepsThe test or command that would prove it

Their ids in the code and in test/eval-results.json are named_target, success_condition, bounded_scope, constraints, repro_included, plan_first and verification.

States what must not change is deliberately not the mirror of Mentions which file or function. Naming the file to work in tells the agent where to start; it does nothing to stop the agent rewriting a neighbouring module on the way past. The first bounds where the work begins, the second bounds how far it can spread, and prompts routinely have one without the other.

Two are conditional. repro_included is only scored on bug reports and plan_first only on large or destructive requests; both applicability questions ride in the same request and are read by code. Everything else is marked n/a rather than counted as a failure.

Not scored at all: slash commands, one-word replies, anything under 15 characters, and Claude Code's own injected messages.

Two modes

Chosen once, stored in ~/.claude/jevpromptcoach/config.json.

on-demand (default) — zero added latency

The hook appends one line to a local JSONL log and exits 0. No API call, no network, no import of the Jev client. Scoring happens later, when you run a command.

Measured cost: 27–31 ms per prompt, of which ~20 ms is Node process startup. The log append itself is well under a millisecond. This is off the network path entirely — it is not waiting on anything, and it cannot delay a response.

always — a short notice, non-blocking

The hook also scores the prompt and prints a short notice while the prompt proceeds. Claude Code prefixes it with UserPromptSubmit says:; the rest is ours:

Jev (Prompt Coach) - 29/100
Missing: which file or function, the verification steps.

Guarantees:

  • Never exit 2. On UserPromptSubmit, exit 2 blocks the prompt and erases what you typed. Every failure path in the hook exits 0.
  • A hard timeout (default 4 s, config timeout <ms>). Jev does not answer in time, nothing prints. A missed score is fine; a stalled prompt is not.
  • * bypasses. A prompt starting with * is never logged or scored.
  • Only findings we can stand behind. Two checks are barred from the inline line entirely on the eval evidence below, and anything near a threshold is dropped rather than shown.

always does not use the mechanism the docs suggest. Writing to stderr with a non-zero exit displays nothing on Claude Code 2.1.277; a top-level systemMessage on exit 0 does. The measurements are in docs/HOOK-BEHAVIOUR.md, along with three other undocumented behaviours worth knowing if you write hooks.

Privacy

Prompts contain code, paths, and sometimes secrets. One of the prompts in this developer's own history contained a live Azure client secret, which is why redaction is a module with tests rather than a regex in passing.

The log is local. ~/.claude/jevpromptcoach/, mode 0600. Nothing leaves your machine except during a command you ran.

LevelWhat is stored and sent
redact (default)Prompt text with credentials, emails and identifying path segments removed. Filenames survive, because named_target is about whether you named one.
metadata_onlyDerived features only — length, word count, has-a-code-fence, has-a-file-path. Never the text. Scoring needs text, so this turns scoring off.
rawPrompt text as written. Credential-shaped strings are still stripped.

Stripped at every level, including raw: sk-, sk-ant-, sk-proj-, ghp_ and friends, AKIA/ASIA, AIza, Slack xox*, JWTs, PEM blocks, Azure client secrets, Bearer tokens, and anything assigned to a name ending in KEY/TOKEN/SECRET/PASSWORD.

Exactly what is sent, and when:

WhenWhat goes to api.typesafe.ai
/jevpromptcoach:scoreThe one prompt you passed, redacted
/jevpromptcoach:reportAny logged prompts not yet scored, redacted, batched
config backfillYour history, redacted, batched — after a cost estimate and an explicit confirmation
always modeEach prompt as you submit it, redacted
Ever, otherwiseNothing

No telemetry. No other network destination. The API key is read from the environment or the key file, and never logged, printed, or included in an error message — error text is scrubbed of it on the way out.

A prompt is scored once. Results are cached by content hash, so unchanged text is never re-sent.

Backfill

/jevpromptcoach:config backfill

Reads ~/.claude/projects/**, finds your human-typed prompts, prints an estimate, and sends nothing until you confirm.

Real numbers from this repository's own development:

Transcripts scanned:      1623 human-typed prompts found
Worth scoring:            1039
Correction-rate pairs:    607
Total:       ~\$0.0531

Actual cost after running it: $0.0618 for 1,039 prompts and 607 pairs.

Claude Code writes one JSONL file per session and marks genuinely typed prompts with promptSource: "typed". Tool results, subagent traffic, compaction summaries and slash-command wrappers all arrive as type: "user" too, and are all excluded. Older records predate that field and are admitted on shape. src/history.ts is the parser.

The outcome signal, and why the report does not use it

Hit rates are self-referential — they say a prompt matched the checks, not that it worked. The plan was to anchor them to correction rate: for each consecutive pair of prompts in a session, did the second one correct the first?

It was validated against 633 real pairs before the report was built around it, and it failed. Six of seven checks show a negative gap — prompts that pass a check are followed by a correction slightly more often, not less — and no gap is significant. The judge itself works; the pairs were read back and it identifies corrections cleanly. Correction rate just does not measure what it was meant to.

The full numbers, the verification that the detector is sound, and what it probably means are in docs/OUTCOME-SIGNAL.md.

So the report ships hit rates and trends only. Per-check correction columns are hidden behind a significance test that this data does not clear, and the report says so rather than implying a correlation. The code stays in, because the gate is data-driven and your history may clear it.

Phase 2, turns-to-completion, is deliberately not built. It faces the same confound.

Eval

40 real prompts from actual history, hand-labelled per check before any model output existed. npm run eval.

The headline metric is fail-precision: of the prompts where the plugin says a habit is missing, how many really were. That is the number that matters, because a missing-habit finding is the only thing always mode ever shows you, and a false one interrupts a message for nothing. Precision beats recall here every time.

Thresholds were tuned on these fixtures, so the figures at those thresholds are optimistic. The number worth quoting is five-fold cross-validated, re-selecting thresholds inside each fold and scoring only held-out prompts:

CheckCV fail-precisionCV fail-recallnInline?
Mentions which file or function0.960.9328yes
States what "done" looks like1.000.8614yes
Keeps to one requirement1.000.6010yes
States what must not change0.970.9730yes
Gives the actual error1.000.805no — 5 cases is too thin
Asks for a plan first0.860.758no — below the 0.90 bar
States the verification steps1.000.9739yes

Applicability gates: is_bug_report 0.93, is_large_change 0.85.

The two checks that do not clear the bar still appear in /jevpromptcoach:score and /jevpromptcoach:report, where you asked. They are barred from the inline line, where you did not.

The fixtures are not committed, by design. They are real prompts from real work — client architecture, internal identifiers, file layouts, and now and then a credential someone pasted in a hurry. There is no safe way to publish that, so what ships is the result: test/eval-results.txt and test/eval-results.json, which are aggregate metrics with no prompt text in them.

You can build and label your own set in a few minutes — node dist/cli.js fixtures-init samples your own history locally, sends nothing, and needs no API key. See test/fixtures/README.md, which also covers what a pull request touching accuracy should include.

Two honest notes on the fixture set:

  • It is stratified, not random. A purely random sample of this history had one prompt naming a verification and no measurable positive class for several checks, so a handful of real prompts carrying the sparse signals were swapped in for near-duplicate short ones. Every prompt is real and unedited.
  • Nine labels were corrected once after the first run, where the original label contradicted the check's own written criteria — seven success_condition labels on prompts that state an action and nothing about what finished looks like, one bounded_scope, one plan_first. The rule was applied mechanically from the criteria text, not per item to agree with the model. It is recorded here because re-labelling after seeing model output is exactly how an eval quietly becomes circular.

Thresholds sit below 0.5 for several checks. Jev's probabilities on these questions run low in absolute terms while ranking prompts well; what matters is the separation, not where it falls.

How it works

UserPromptSubmit ──► hook.ts ──► redact ──► append JSONL ──► exit 0
                                                              (on-demand: stops here)
/jevpromptcoach:score  ─┐
/jevpromptcoach:report ─┼──► one batched request ──► api.typesafe.ai/v1/systemone
config backfill        ─┘                            model: jev-latest

Every question is a Noul — a yes/no question returning a calibrated probability. The seven checks and two gates are nine Nouls in one request. A backfill packs up to 60 prompts into a single request, sized against the 64k total and 32k state-only budgets.

Noul answers carry no confidence field, unlike Choice and Score. Certainty is read from the probability's distance from the threshold, which is what the inline margin gates on.

The scoring model is jev-latest (currently jev-1.13.0) and there is no fallback to any other provider. If Jev does not answer, nothing is scored and the command says so.

The one thing Jev does not do is write. /jevpromptcoach:score produces the rewrite through your own agent, from Jev's verdicts and your repository — Jev answers typed questions and cannot generate text. All measurement is Jev's.

Source layout: src/checks.ts defines the questions, thresholds and inline eligibility. src/score.ts batches and interprets. src/hook.ts is the critical path. src/redact.ts is the privacy boundary. src/report.ts aggregates and runs the significance test. src/history.ts parses Claude Code transcripts.

Contributing

CONTRIBUTING.md has the detail. Two rules are absolute: no credential and no prompt text ever reaches a commit — yours or anyone's.

That is enforced rather than asked for. scripts/check-leaks.mjs runs as a pre-commit hook (installed by npm install), as part of npm test, and again in CI on every pull request. --no-verify skips the hook, not CI.

npm install          # also points git at the repo's hooks
npm test             # build, lint, tests, leak scan — no API key, no network
npm run typecheck
node dist/cli.js fixtures-init   # build your own eval set, locally, from your history
npm run eval                     # calls Jev; ~\$0.002 for 40 prompts
npm run eval -- --cached         # recompute metrics from the last run, no API calls

The eval needs a labelled test/fixtures/prompts.json, which is gitignored. Without one, npm test and the build still work — only npm run eval needs it.

The tests cover the two things that must not regress: that redaction removes every credential shape it claims to, and that the hook sends redacted text and exits 0 on every path. The hook test asserts on the actual request body, against a local capture server, because the bug it exists to catch was a caller passing the raw prompt to a function that does no redaction of its own.

Why dist/ is committed, and why there is no lockfile

Both come from the same measurement.

Claude Code installs a plugin's dependencies with --ignore-scripts, so no build ever runs at install time. Without a committed dist/, the hook would point at a file that does not exist. The bundle is self-contained — the hook runs with no node_modules at all, which is also what keeps it at 27 ms.

Given that, a lockfile is pure cost. Claude Code runs npm ci when a plugin has both a package.json and a lockfile, and it installs devDependencies: 43 MB of esbuild and TypeScript in every user's plugin cache, on a 60-second timeout, of which nothing is used. Dropping the lockfile skips that step entirely.

plugin cache
with a lockfile44 MB
without376 KB

Reproducibility is kept by pinning every devDependency to an exact version and by CI failing if a fresh build of src/ differs from the committed dist/. CI also fails if a lockfile reappears, because re-adding one is an easy and invisible way to put the 43 MB back.

The remaining cost of committing a bundle is diff noise, and .gitattributes marks dist/ as generated so GitHub collapses it in pull requests. Running the TypeScript directly instead — Node can strip types natively now — was measured and rejected: 51 ms against 25 ms for the compiled bundle, on a hook whose whole claim is that it stays out of the way.

Licence

MIT, for the code. See LICENSE.

The names and logos are not covered by it — fork the code, but rename the fork and take the Crowdlinker mark off it. TypeSafe, Jev, Claude and Claude Code belong to their own owners. TRADEMARKS.md sets out who claims what.


Created with love by Crowdlinker

Measurement, not vibes. If a number here is wrong, open an issue with what you measured.