A Claude Code skill that sends non-urgent work to the Anthropic Message Batches API at 50% cost.
Code reviews, documentation, architecture analysis, refactoring plans, security audits — anything that can wait ~1 hour gets half-price processing with Claude Opus (or any batch-capable Anthropic model).
This is the skill-only variant: no MCP server, no long-running process. The /batchy skill drives a single Python CLI (batch.py) via uv run. All state is on disk under ~/.claude/batches/.
Fork of s2-streamstore/claude-batch-toolkit — strips out the MCP server and Vertex AI backend, adds safety hardening. See What this fork changes and Credits.
Upstream supports two backends (Anthropic Message Batches + Vertex AI BatchPredictionJobs) and ships an MCP server wrapping the CLI. This fork strips both layers down to a single-backend, skill-only toolkit. Concretely:
- No MCP server. Every MCP tool already had a CLI equivalent and all state lived on disk, so the MCP layer was pure ceremony. The
/batchyskill now invokesbatch.pydirectly via shell. NomcpServers.claude-batchentry in~/.claude.json, nomcpPyPI dependency, no "MCP server not responding" failure mode, every batch action visible as aBashtool call. - No Vertex AI backend. Removed the
VertexBatchBackendclass, thegoogle-auth/google-cloud-storagePyPI deps, theVERTEX_*env vars, the--backendCLI flag, and the GCS upload/download paths. If you need Vertex, use upstream. - Tightened
packet_pathallowlist. The CLI rejects packet files outside cwd / tempdir /~/.claude/batches/(override viaCLAUDE_BATCH_PACKET_ROOTS) to defeat prompt-injection-driven file exfiltration. - Safer statusline. Replaced
source ~/.claude/envwith agrep | sedextraction so a tampered env file can no longer execute arbitrary shell on every assistant turn. - Cross-process jobs.json lock. A
.jobs.lock.ddirectory mutex shared bybatch.pyandstatusline.shprevents either process from clobbering the other's updates. poll_oncelogs to stderr instead of silently swallowing exceptions (setCLAUDE_BATCH_DEBUG=1for tracebacks).- Default model bumped
claude-opus-4-6→claude-opus-4-8; default max_tokens32768→65536. Opt-in auto-tracking viaCLAUDE_MODEL=auto-opus(newest Opus) orauto-latest(newest in any family, may pick Fable 5) — see Auto-tracking the latest model. - PyPI deps pinned to compatible ranges. Offline test suite under
tests/.
Net change vs upstream: roughly -450 lines of code/docs, plus +250 lines of tests.
Vertex jobs from a prior install will still appear in list (the loader filters legacy fields gracefully) but cannot be polled — re-submit them through Anthropic, or use upstream.
git clone git@github.com:pdalinis/claude-batchy.git
cd claude-batchy
./install.sh --api-key sk-ant-your-key-hereThe installer shows a manifest of every change it will make and asks for confirmation before proceeding.
| Flag | Description |
|---|---|
--api-key KEY |
Your Anthropic API key (required unless already in env) |
--no-poller |
Skip status line configuration |
--unattended |
No interactive prompts |
./uninstall.shThis shows what will be removed, asks for confirmation, and preserves your results in ~/.claude/batches/results/. Use --purge-data to also remove results.
Manual Installation (no script)
If you prefer not to run the install script — or need to install in a restricted environment — follow these steps to set up each component by hand.
| Dependency | Purpose | Install |
|---|---|---|
| uv | Runs the Python CLI (no virtualenv needed) | curl -LsSf https://astral.sh/uv/install.sh | sh |
| jq | JSON processing in statusline + installer | brew install jq or apt-get install jq |
| curl | Polls the Anthropic API from statusline | brew install curl or apt-get install curl |
You also need an Anthropic API key (sk-ant-...). Get one from console.anthropic.com.
mkdir -p ~/.claude/skills/batchy
mkdir -p ~/.claude/batches/resultscp skills/batchy/batch.py ~/.claude/skills/batchy/batch.py
cp skills/batchy/SKILL.md ~/.claude/skills/batchy/SKILL.md
chmod +x ~/.claude/skills/batchy/batch.pySkip this step if you don't want batch job counts in your Claude Code status bar. Everything else works without it.
cp statusline.sh ~/.claude/statusline.sh
chmod +x ~/.claude/statusline.shThe toolkit reads ANTHROPIC_API_KEY from ~/.claude/env. This file must be mode 600.
echo 'export ANTHROPIC_API_KEY="sk-ant-YOUR-KEY-HERE"' > ~/.claude/env
chmod 600 ~/.claude/envIf ~/.claude/env already exists, edit it to add/replace the ANTHROPIC_API_KEY line, then chmod 600 ~/.claude/env.
Skip if you skipped Step 3.
jq -n --arg cmd "bash $HOME/.claude/statusline.sh" '{
"statusLine": {"type": "command", "command": $cmd}
}' > ~/.claude/settings.jsonIf ~/.claude/settings.json already exists, merge instead of overwriting:
jq --arg cmd "bash $HOME/.claude/statusline.sh" '
.statusLine = {"type": "command", "command": $cmd}
' ~/.claude/settings.json > ~/.claude/settings.json.tmp \
&& mv ~/.claude/settings.json.tmp ~/.claude/settings.jsonif [ ! -f ~/.claude/batches/jobs.json ]; then
echo '{"version": 1, "jobs": {}}' | jq '.' > ~/.claude/batches/jobs.json
fisource ~/.claude/env
uv run ~/.claude/skills/batchy/batch.py listExpected: empty JSON array or a list of existing jobs. First run may take a moment while uv resolves dependencies.
# Toolkit files
rm -f ~/.claude/skills/batchy/batch.py
rm -f ~/.claude/skills/batchy/SKILL.md
rm -f ~/.claude/statusline.sh
rmdir ~/.claude/skills/batchy 2>/dev/null || true
rmdir ~/.claude/skills 2>/dev/null || true
# Statusline (if installed)
jq 'del(.statusLine)' ~/.claude/settings.json > ~/.claude/settings.json.tmp \
&& mv ~/.claude/settings.json.tmp ~/.claude/settings.json
# API key (optional)
grep -v '^export ANTHROPIC_API_KEY=' ~/.claude/env > ~/.claude/env.tmp \
&& mv ~/.claude/env.tmp ~/.claude/env && chmod 600 ~/.claude/env
[ ! -s ~/.claude/env ] && rm -f ~/.claude/env
# Jobs data (optional)
# rm -rf ~/.claude/batchesThe skill was originally named /batch, but Claude Code introduced a built-in /batch command with different semantics (#4). To avoid the collision, we renamed to /batchy. Existing installs are migrated automatically when you re-run ./install.sh.
If you previously installed the MCP-based upstream version, ./install.sh cleans up ~/.claude/mcp/ and removes the mcpServers.claude-batch entry automatically. See What this fork changes for the full rationale.
In Claude Code, just say:
/batchy Review this codebase for security issues
/batchy Generate comprehensive tests for src/auth/
/batchy Write API documentation for all public endpoints
Claude will gather all relevant context, build a self-contained prompt file with mktemp, run batch.py submit, and tell you the job ID.
/batchy check
/batchy status
/batchy list
Results appear in your status bar automatically. When a job completes, Claude reads the result file from disk and presents it.
The same CLI also works standalone:
# Submit a job
uv run ~/.claude/skills/batchy/batch.py submit --packet-path prompt.md --label "security-review"
# List all jobs
uv run ~/.claude/skills/batchy/batch.py list
# Poll for completed jobs
uv run ~/.claude/skills/batchy/batch.py poll
# Fetch a specific result
uv run ~/.claude/skills/batchy/batch.py fetch msgbatch_xxx --print┌─────────────────────────────────────────────────────────────────┐
│ Claude Code Session │
│ │
│ User: "/batchy review src/ for security issues" │
│ │
│ Claude: │
│ 1. mktemp → $TMPDIR/batchy-XXXXXX.md │
│ 2. Assembles self-contained prompt (bash cat → temp file) │
│ 3. Runs: uv run ~/.claude/skills/batchy/batch.py submit ... │
│ 4. Parses {"job_id": "msgbatch_..."} from stdout │
│ 5. Reports: "Submitted job msgbatch_abc123" │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Status Bar │ │
│ │ [Opus] 42% | $1.23 | batch: 1 pending │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ... ~30 minutes later ... │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Status Bar │ │
│ │ [Opus] 42% | $1.23 | batch: 1 done │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ User: "/batchy check" │
│ Claude: cat ~/.claude/batches/results/msgbatch_abc123.md │
│ presents formatted results │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────┐
│ Anthropic Batch │
│ API (direct) │
│ 50% cost │
│ ~1hr turnaround │
└──────────────────┘
The status line is the only moving part — no daemons, no background services, no launchd/systemd.
Assistant message arrives
│
▼
statusline.sh runs
│
├─► Render (instant): Read jobs.json → print status bar
│
└─► Poll (async fork): If pending jobs + cache stale (>60s)
└─► curl Anthropic API → update jobs.json
(never blocks the status line)
| Property | Value |
|---|---|
| Blocks status line? | Never — poll is forked |
| Polls when idle? | No — only during active Claude sessions |
| Poll frequency | At most once per 60s |
| Extra processes | None — no daemon |
| Wasted API calls | Zero when no pending jobs |
| Variable | Default | Description |
|---|---|---|
ANTHROPIC_API_KEY |
— | Your Anthropic API key (required) |
CLAUDE_BATCH_DIR |
~/.claude/batches |
Where jobs.json and results live |
CLAUDE_MODEL |
claude-opus-4-8 |
Model id, or one of the sentinels auto-opus / auto-latest (see Auto-tracking the latest model) |
CLAUDE_MAX_TOKENS |
65536 |
Max output tokens (Opus 4.7+ supports up to 128k sync / 300k batch-beta) |
CLAUDE_THINKING |
— | Set to enabled for extended thinking |
CLAUDE_THINKING_BUDGET |
— | Token budget for thinking |
The default CLAUDE_MODEL is pinned (currently claude-opus-4-8) for reproducibility — a batch you submit today and another tomorrow use the same model. When Anthropic ships a new model, the default goes stale.
Two sentinel values track Anthropic's catalog automatically. Both call the Models API (GET /v1/models) on submit, filter for capabilities.batch.supported, take the newest match (the API returns entries newest-first), and cache the result for 24 hours under ~/.claude/batches/.model_cache.json. Cache miss adds ~300ms; the cache is keyed by sentinel so switching between them doesn't pick up stale values.
| Sentinel | What it picks | When to use |
|---|---|---|
CLAUDE_MODEL=auto-opus |
Newest batch-capable model whose id starts with claude-opus- |
You want auto-tracking with predictable cost behavior — Opus pricing has been stable across versions |
CLAUDE_MODEL=auto-latest |
Newest batch-capable model in any family (may select Fable 5) | You want the most capable available model regardless of cost or family |
Or do a one-shot lookup and pin the result yourself:
# Print the newest batch-capable Opus
uv run ~/.claude/skills/batchy/batch.py latest-opus
# → claude-opus-4-8
# Print the newest batch-capable model in any family
uv run ~/.claude/skills/batchy/batch.py latest-model
# → claude-fable-5 (as of writing)
# Pin into your env file
echo "export CLAUDE_MODEL=$(uv run ~/.claude/skills/batchy/batch.py latest-opus)" >> ~/.claude/envThe latest-* subcommands bypass the cache so you always get a fresh value.
Reproducibility vs currency. Auto-tracking is great for getting Anthropic improvements for free. It's bad if you run periodic evals or compare outputs across days — same prompt + different model = different output. Pin a specific id for those.
Cost note for
auto-latest. Fable 5 is roughly 2× the cost of Opus 4.x (see cost table above).auto-latestcould silently double per-job spend the next time Anthropic ships a Fable-tier model.auto-opuskeeps you in the cheaper tier.
If you pin CLAUDE_MODEL=claude-fable-5 directly (or use auto-latest):
- It costs ~2× Opus 4.x for the same job. The 50% Batch API discount still applies.
- It supports only adaptive thinking (always on), not the extended thinking config. If you also set
CLAUDE_THINKING=enabled, submissions will likely 400 — leave that env var unset when using Fable. - Max output is 128k (same as Opus 4.7+). The 300k batch-beta header is currently advertised only for Opus 4.6/4.7/4.8 and Sonnet 4.6 — Fable may or may not honor it.
~/.claude/
├── env # ANTHROPIC_API_KEY (mode 600)
├── settings.json # statusLine config
├── skills/
│ └── batchy/
│ ├── SKILL.md # Skill definition (read by Claude Code)
│ └── batch.py # CLI (invoked via `uv run`)
├── statusline.sh # Status bar + cached poller
└── batches/
├── jobs.json # Job registry
├── .poll_cache # Last poll timestamp
├── .poll.lock # Prevents concurrent polls
└── results/
├── msgbatch_xxx.md # Completed results
└── msgbatch_xxx.meta.json
| Model | Standard | Batch (50% off) |
|---|---|---|
Claude Fable 5 (claude-fable-5) |
$10 / $50 per 1M tokens | $5 / $25 |
Claude Opus 4.7 / 4.8 (default: claude-opus-4-8) |
$5 / $25 per 1M tokens | $2.50 / $12.50 |
| Claude Opus 4.5 and earlier | $15 / $75 per 1M tokens | $7.50 / $37.50 |
| Claude Sonnet 4.x | $3 / $15 per 1M tokens | $1.50 / $7.50 |
| Claude Haiku 4.5 | $1 / $5 per 1M tokens | $0.50 / $2.50 |
(Input / Output per million tokens. Verified against Anthropic docs 2026-06-10.)
Typical turnaround: under 1 hour. Maximum: 24 hours.
Max output: Opus 4.7+ supports up to 128k tokens synchronously, and up to 300k on the Batch API with the
output-300k-2026-03-24beta header. The toolkit's defaultCLAUDE_MAX_TOKENSis 64k — override the env var to go higher.
# Test the CLI directly
uv run ~/.claude/skills/batchy/batch.py list
# Check if uv is installed
which uv
# Verify API key
grep ANTHROPIC_API_KEY ~/.claude/env# Check statusline config
jq '.statusLine' ~/.claude/settings.json
# Test statusline manually
echo '{}' | bash ~/.claude/statusline.sh
# Check jobs.json exists
cat ~/.claude/batches/jobs.json# Manual poll
uv run ~/.claude/skills/batchy/batch.py poll
# Check API status directly
source ~/.claude/env
curl -s -H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
https://api.anthropic.com/v1/messages/batches/BATCH_IDchmod 600 ~/.claude/env- CLI (
skills/batchy/batch.py): Python script run byuv. Subcommands:submit,status,fetch,list,poll. Talks directly to the Anthropic Message Batches API. - Skill (
skills/batchy/SKILL.md): Teaches Claude Code how and when to invoke the CLI. Loaded automatically when Claude Code starts. - Status Line (
statusline.sh): Bash script that renders batch job counts in the Claude Code status bar and triggers background polling viacurl+jq. Readsjobs.jsondirectly — no Python involved on the fast path. - Jobs Registry (
jobs.json): JSON file tracking all submitted batch jobs, their states, and result paths. Written atomically by the CLI and the statusline.
This is a fork of s2-streamstore/claude-batch-toolkit by Cristian C (cristian@s2.dev). The original design and implementation — Anthropic + Vertex backends, the cached status-line poller, the install/uninstall scripts, and the /batchy skill itself — is his work. This fork drops the Vertex backend; if you need it, use upstream.
The skill-only refactor (removing the MCP server layer in favor of direct CLI invocation, plus the safety hardening described in What this fork changes) is by Peter Dalinis.
MIT — see LICENSE. Copyright is held jointly by s2-streamstore (Cristian C) and Peter Dalinis.