A personal agent CLI plugin that marks up every prompt you type like a teacher with a red pen — scoring your phrasing, highlighting what's broken, and showing how a native speaker would say the same thing — all in your chosen target language. Designed for developers who want passive writing practice while doing their day job at the terminal.
Currently supports two agent CLIs as separate but feature-parallel plugins, plus a desktop launcher for the unified ChatGPT App:
- Claude Code — install
redpen - OpenAI Codex CLI — install
redpen-codex - ChatGPT App — run the lightweight launcher in ChatGPT App launcher
The two CLI plugins share the same architecture (UserPromptSubmit hook +
systemMessage emit), scoring rubric, target languages, and shared
render_diff.py. The ChatGPT App launcher reuses the Codex runner but renders
feedback in the app DOM instead of a CLI hook channel. Your original prompt
always reaches the model unchanged — the feedback is shown to you only, never
added to the model's context. redpen is a coach, not a rewriter: the goal
is for you to read the correction, notice what was off, and write a little
better next time.
Supported languages (both plugins):
- English
- 中文 (Chinese)
- Español (Spanish)
- 日本語 (Japanese)
Each time you submit a prompt, the plugin scores your phrasing, shows a corrected version with the changes diffed inline (red strikethrough for what was removed, green for what was added), and (optionally) a "native style" line showing how a native speaker would phrase the same thing:
In Claude Code:
You: help me fix the bug, the app crash when click button
redpen: [62] help me fix the bug — the app crashes when I click the button.
──── Native style ────
any idea why the app crashes whenever I click that button?
The assistant then proceeds to answer your original prompt normally.
In Codex CLI the same 3 rows appear, but the divider and native-style
line start at the left margin (no indentation), because the plugin pads
each section with spaces to the terminal column boundary so natural wrap
produces the row breaks — Codex's systemMessage channel is a
single-string toast that strips all newlines (see
Platform differences):
You: help me fix the bug, the app crash when click button
redpen: [62] help me fix the bug — the app crashes when I click the button.
──── Native style ────
any idea why the app crashes whenever I click that button?
The assistant then proceeds to answer your original prompt normally.
The feedback is delivered via the host CLI's systemMessage channel
(supported by both Claude Code and Codex), so it's visible to you but
never added to the model's context — the assistant only ever sees
your original wording, and your conversation stays clean.
The two plugins are feature-parallel, but each host CLI has its own constraints:
Claude Code (redpen) |
Codex CLI (redpen-codex) |
|
|---|---|---|
| Config | ~/.claude/redpen.config |
~/.codex/redpen.config |
| Default model | haiku (alias), user-configurable via /redpen:setup |
gpt-5.6-luna Fast, with automatic fallback to gpt-5.6-terra Fast and then gpt-5.4-mini Standard |
| Setup invoke | /redpen:setup |
$redpen-setup (Codex skill — TUI only) |
| Hook target | claude -p |
codex exec |
| Output layout | multi-line (score / divider / native style) | visually 3 rows (score / divider / native style), same divider label as Claude Code — Codex's systemMessage channel is a single-string toast that strips all newlines, so the plugin pads each section to the terminal column boundary and relies on natural terminal wrap for the row breaks |
The two configs live at independent paths (~/.claude/redpen.config vs
~/.codex/redpen.config), so both plugins can be installed side-by-side
without colliding.
# 1. Register this repo as a marketplace
/plugin marketplace add 12og3r/redpen
# (https://github.com/12og3r/redpen also works)
# 2. Install the plugin
/plugin install redpen@redpen
# 3. Restart Claude Code so the UserPromptSubmit hook registers# 1. Register this repo as a Codex marketplace
codex plugin marketplace add 12og3r/redpen
# 2. Install the Codex plugin (note the @redpen marketplace suffix)
codex plugin add redpen-codex@redpenDefaults: out of the box (no config file), redpen-codex coaches in
English with the native-style hint on. Send any prompt and you
should see a [NN] <rewrite> → <native-style> line. No setup
required.
This path does not modify ChatGPT.app or unpack app.asar. It launches a
fresh ChatGPT process with Chrome DevTools remote debugging enabled, injects
a small renderer script through CDP, and handles redpen checks through a
Runtime.addBinding bridge back to this launcher.
# Quit ChatGPT first, then run from source:
cargo run -p redpen-codex-app -- launchFor a released build, download RedPen.dmg, open it, and drag Red Pen(ChatGPT) to Applications.
The launcher reuses the OpenAI plugin config at ~/.codex/redpen.config and the same
codex exec runner as redpen-codex. Feedback appears asynchronously under
the just-submitted user message. Your original prompt is not changed, and the
feedback is not sent back into the conversation context.
Useful flags:
cargo run -p redpen-codex-app -- launch \
--chatgpt-app /Applications/ChatGPT.app \
--debug-port 9229If ChatGPT is already running, the launcher exits with an instruction to quit it first. This is intentional: an already-running Electron process may ignore new remote-debugging arguments.
Release builds are single-file executables. The binary embeds the redpen
coach scripts and expands them under
~/Library/Application Support/redpen-chatgpt-app/runtime/<version>/ on first
run. Set REDPEN_COACH_SCRIPT or pass --coach-script only when debugging a
local script override.
The launcher defaults to /Applications/ChatGPT.app. Older Codex desktop
installs are still supported as a fallback, and --codex-app remains an alias
for --chatgpt-app.
Run the bundled setup command (after a session restart):
/redpen:setup
This asks three questions:
| Question | Choices |
|---|---|
| Language | English · 中文 (Chinese) · Español (Spanish) · 日本語 (Japanese) |
| Model | Haiku (default, recommended) · Sonnet · Opus |
| Native style line | On (default, recommended) · Off |
The chosen values are written to ~/.claude/redpen.config. You can also edit
that file by hand:
LANGUAGE=chinese
MODEL=haiku
SHOW_HINT=on
MODEL accepts the generic family aliases haiku / sonnet / opus —
claude --model resolves these to the latest released version, so this
config keeps working across Anthropic model releases without a plugin
update. Power users can also pin a specific version
(e.g. MODEL=claude-haiku-4-5-20251001) — any value claude --model
accepts will work. Pick Other in /redpen:setup to type a
custom value. Set MODEL= (empty) to follow whatever Claude Code's
/model is currently set to instead.
In a Codex TUI session, type:
$redpen-setup
The skill walks two numbered questions:
| Question | Choices |
|---|---|
| Language | English · 中文 (Chinese) · Español (Spanish) · 日本語 (Japanese) |
| Native style line | On (default, recommended) · Off |
The chosen values are written to ~/.codex/redpen.config. The skill doesn't
ask about the model because the runner automatically selects the fastest
available option from its fallback chain.
You can also edit ~/.codex/redpen.config by hand (just 2 lines — see
the example in plugins/redpen-codex/skills/setup/SKILL.md);
this is the only route in non-TUI codex exec since skills don't fire
there.
- Single-string output channel. Codex's
systemMessagehook channel renders as a single-string warning toast that strips all newlines (verified empirically —\n,\n\n,\r,<br>, U+2028, markdown hard break, all collapse). The Codex plugin works around this by padding each section (score line / divider / native-style line) with spaces to the terminal column boundary, letting natural terminal wrap create the visual row breaks. The visual result is the same 3-row layout as Claude Code; the divider label (──── Native style ────etc.) is identical. - Models use an automatic speed-first fallback chain. Redpen first tries
gpt-5.6-lunawith Fast mode. If that request fails or returns no output, it retries withgpt-5.6-terrain Fast mode, then falls back togpt-5.4-miniin Standard mode. - Skills are TUI-only. The
$redpen-setupskill only fires inside the interactive Codex TUI. Incodex execnon-interactive mode the skill invocation does nothing; users on that path should edit~/.codex/redpen.configby hand instead. - No
--no-toolsanalog incodex exec— tool definitions still inflate the prompt context (~5–7k tokens observed) vs. the Claude Code version. Latency and cost are higher per coach turn. codex execflag stack is empirical. We use--ephemeral,--ignore-user-config,--ignore-rules,--skip-git-repo-check,--sandbox read-only,-c model_reasoning_effort=low. The combination works on Codex 0.133.0 + ChatGPT-account auth (verified end-to-end manually). If something is slow on your machine, file an issue with timings.- Latency depends primarily on the model round trip. The fallback chain
prefers Fast-mode GPT-5.6 models before trying
gpt-5.4-mini. Codex CLI startup is ~50ms; the rest is OpenAI network + model inference and is per-call. There's nothing to prewarm here —codex execdoesn't have CLI-level overhead to amortize (and neither does the Claude Code binary anymore: it ships as a native executable, so the per-call cost is the model round-trip, not CLI startup).-c model_reasoning_effort=minimalis faster but produces empty output (verified — model refuses to generate at that effort level), so unusable. Routes for faster turnaround if you need it: switch toOPENAI_API_KEYauth + direct API call (bypass the codex wrapper), or run a local model via--oss --local-provider ollama(loses quality).
Each plugin bundles its own shared/ directory (coach_prompts.sh,
render_diff.py) so marketplace installs are self-contained — the installer
copies a single plugin directory and a sibling shared/ would not come
along. The three plugins (redpen, redpen-codex, redpen-coco) maintain
their shared/ copies independently: there is no canonical source and no
sync step, so each plugin can diverge where its host CLI needs it (e.g. coco
skips the leading newline its TUI already adds). When fixing a bug that
affects more than one plugin, apply the change to each plugin's copy by hand.
The model scores your original prompt on a 0–100 scale:
| Score | Meaning |
|---|---|
| 100 | Already perfect, natural, idiomatic |
| 80–99 | Minor polish (article / preposition / tense slips) |
| 50–79 | Understandable but with clear grammar or word-choice errors |
| 1–49 | Broken, hard to read |
| 0 | Contains ANY character from a non-target language (even one foreign letter forces 0, regardless of the rest) |
The correction always runs — even on score 0 — so you always see a target-language version, even when your input was in a different language. Brand names, file paths, code identifiers, and function names are preserved verbatim.
To avoid burning model calls on inputs that aren't natural-language prose:
- Empty prompts
- Pure slash commands (e.g.
/help) — when a slash command is followed by space-separated args, those args ARE coached - (Codex only) Pure skill invocations (e.g.
$redpen-setup) — same args rule as slash commands; the$cmd <text>form coaches just the args - Shell passthroughs (
!ls,!ls -la) - Prompts longer than
MAX_PROMPT_CHARScharacters (default2000). The UserPromptSubmit hook doesn't receive paste metadata from the host CLI, so we can't surgically separate user-typed prose from pasted code, logs, or transcripts. Length is the simplest reliable proxy — long prompts almost always contain paste we don't want to rewrite. Tune via env var or by addingMAX_PROMPT_CHARS=<n>to yourredpen.config(the Claude Code plugin reads~/.claude/redpen.config; the Codex plugin reads~/.codex/redpen.config).
The Codex CLI version follows the same shape (UserPromptSubmit hook
spawns codex exec, parses output, emits systemMessage); see
plugins/redpen-codex/hooks/grammar_check.sh for its exact flag stack.
┌─────────────────────────────────────────────────────────┐
│ You submit a prompt in Claude Code │
└──────────────────────┬──────────────────────────────────┘
▼
UserPromptSubmit hook fires
│
▼
┌─────────────────────────────────────────┐
│ Strip /cmd token if present │
│ Skip empty / shell / pure-slash prompts │
└─────────┬───────────────────────────────┘
▼
Spawn `claude -p` from $TMPDIR with the
minimal-startup flag stack:
• --system-prompt = coach instructions
• --setting-sources "" no user/proj/local settings
• --strict-mcp-config skip default MCP config
• --mcp-config '{...}' empty MCP config
• --no-session-persistence no transcript .jsonl
• --tools "" no tool defs (drops ~11k input tokens)
• --effort low skip the model's internal thinking block
• </dev/null skip 3s stdin wait
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
+ CLAUDE_CODE_DISABLE_AUTO_MEMORY=1
+ CLAUDE_CODE_DISABLE_CLAUDE_MDS=1
+ CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS=1
+ REDPEN_ACTIVE=1 (recursion guard)
│
▼
Receive "[NN] <rewrite>"
│
▼
Emit JSON {"systemMessage": "\n[NN] <rewrite>"}
│
▼
Claude Code displays it inline to you;
parent conversation context is unchanged.
Key design choices:
- Synchronous on purpose. An earlier iteration tried async (UserPromptSubmit forks a detached worker → Stop hook drains a queue file when Claude finishes responding). It worked but the score appeared in unpredictable positions — far below the original prompt, sometimes interleaved with tool output. Reverting to a blocking call keeps the score pinned directly under your prompt where you can compare side-by-side.
--system-promptreplaces the default Claude Code system prompt so the coach instructions aren't diluted.- The minimal-startup flag stack + env vars cut
claude -poverhead roughly in half on the OAuth/Pro auth path (none of these flags require an API key —--barewould, but it'd break for subscription users). The single biggest win is</dev/nullredirecting stdin, which kills a hard-coded 3-second wait insideclaude -p. - The headless call runs from
$TMPDIR//tmp/$HOME(first that exists) to escape project-levelCLAUDE.mdand auto-loaded skills (e.g.superpowers:systematic-debuggingwould otherwise hijack "fix the bug" prompts). --no-session-persistencemeans no.jsonltranscript ever gets written, so we don't need--session-idtracking or a cleanup pass.
(For Codex-specific limitations see Codex CLI — known limitations above.)
- Adds ~1–3s of latency before your prompt reaches the model (median ~1.2s with Haiku, ~2.2s with Sonnet, on the OAuth/Pro auth path, measured over a 35-prompt bench varying from 7 to 523 chars). Latency scales mildly with prompt length.
- Haiku gets a special-case optimization stack. Haiku 4.5 forces adaptive
extended thinking even with
--effort low— out of the box, median latency is 9s (p95 32s) and output explodes to 742 tokens median (p95 3771). WhenMODEL=haiku*, the hook setsCLAUDE_CODE_DISABLE_THINKING=1and appends a visibleANALYSIS:reasoning line plus ~20 few-shot examples to the system prompt. That collapses median latency to ~1.2s (p95 2.8s) and median output to 60 tokens, while keeping the false-zero rate at 0/100 (a nakedDISABLE_THINKINGcauses Haiku to misjudge ~5% of clean English as score 0). The few-shot examples push the system prompt past Haiku 4.5's ~4096-token prompt-cache threshold, so subsequent calls within the cache window hitcache_readand pay 10% of input cost. Net result: Haiku is the cheapest and fastest option for this task — 61% cheaper than its uncached form, 47% cheaper than Sonnet, and 26% faster than Sonnet on p80 latency. - Opus gets a slim system prompt + Sonnet fallback. When
MODEL=opus*andLANGUAGE=english, the hook swaps to a 4× shorter system prompt (the verbose nuance / examples don't help Opus 4.7 follow the rules). Bench: -62% cost, -34% p95 latency, -56% max latency, 0 false-zeros. It also passes--fallback-model sonnetso when Opus is queue-overloaded (the cause of its p95 long tail — not hidden thinking, which is already off at--effort low) the request falls through to Sonnet rather than wait. Quality is preserved because Sonnet is Opus-quality on this task. - Costs ~$0.002–0.006 per call. With
--tools ""the input drops below Sonnet's prompt-cache threshold for English/Spanish (so no caching, but also no cache-creation premium). Longer system prompts like Chinese/Japanese still trigger caching automatically. - Spanish vs English vs other Latin-script languages are not character-level distinguishable; for Spanish mode, the model decides Spanish-ness from vocabulary and grammar.
- The
systemMessagefield is read but discarded by some pipe-based tools; the feedback only renders in interactive Claude Code sessions.
redpen counts anonymous installs across all four channels (Claude Code
plugin, Codex CLI plugin, ChatGPT App, coco/Trae CLI plugin) so we have a rough
sense of usage. The only thing ever sent is a fixed channel label (claude /
codex-cli / chatgpt-app / coco) — no prompt text, no IP (the counting
Worker never reads it),
no machine id, no user data of any kind. Each client fires the ping once
per installed version (a local marker file stores the version), so the
totals grow with every install and every update, while an idle user on one
version is never re-counted.
Opt out completely at any time:
export REDPEN_NO_TELEMETRY=1The counter is a ~40-line Cloudflare Worker that stores only integers; its full
privacy contract, deploy steps, and the live /stats endpoint are documented
in telemetry/README.md. The client pings are no-ops
until you deploy your own Worker and fill in its URL (the repo ships with a
placeholder). The ChatGPT App launcher is additionally counted natively by GitHub's
Release asset download stats.
redpen/
├── README.md ← this file
├── LICENSE
├── telemetry/ ← anonymous install counter (Cloudflare Worker)
│ ├── worker.js ← KV counter, never reads IP/headers
│ ├── wrangler.toml
│ └── README.md ← deploy + privacy contract
├── .claude-plugin/marketplace.json ← Claude Code marketplace entry
├── .agents/plugins/marketplace.json ← ChatGPT + Codex marketplace entry
├── plugins/redpen/ ← Claude Code plugin
│ ├── .claude-plugin/plugin.json
│ ├── commands/setup.md ← /redpen:setup
│ ├── shared/ ← coach_prompts.sh + render_diff.py (self-contained)
│ └── hooks/
│ ├── hooks.json ← UserPromptSubmit registration
│ └── grammar_check.sh ← the hook itself
├── plugins/redpen-codex/ ← ChatGPT + Codex plugin (stable package ID)
│ ├── .codex-plugin/plugin.json
│ ├── skills/setup/SKILL.md ← $redpen-setup
│ ├── shared/ ← own copy (also embedded in the ChatGPT launcher)
│ └── hooks/
│ ├── hooks.json
│ └── grammar_check.sh
└── plugins/redpen-coco/ ← coco CLI plugin (own shared/ copy)
Each plugin's shared/ is maintained independently — there is no canonical
copy and no sync step (see Developing).
User-level files (created on first run):
~/.claude/redpen.config— Claude Code plugin: language + model + hint~/.claude/redpen.log— Claude Code plugin debug log (rotates manually)~/.codex/redpen.config— Codex plugin: language + hint (model is locked)~/.codex/redpen.log— Codex plugin debug log
MIT — see LICENSE.
Inspired by jiang1997/claude-code-language-coach — thanks to the original author for the idea of using a UserPromptSubmit hook to coach the user's writing inside Claude Code.