commit 7478aef719c5d929a2d4a1c5ee25f59ea58c17f5 Author: Daniel Newman Date: Fri May 15 11:55:11 2026 -0400 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..482e34b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +*.swp +*.swo diff --git a/Formula/burn.rb b/Formula/burn.rb new file mode 100644 index 0000000..b7a92c2 --- /dev/null +++ b/Formula/burn.rb @@ -0,0 +1,19 @@ +class Burn < Formula + desc "Token-burning harness around Claude Code" + homepage "https://github.com/dtnewman/burn_baby_burn" + url "https://github.com/dtnewman/burn_baby_burn/archive/refs/tags/v0.1.0.tar.gz" + sha256 "REPLACE_WITH_RELEASE_TARBALL_SHA256" + license "MIT" + head "https://github.com/dtnewman/burn_baby_burn.git", branch: "main" + + depends_on "jq" + + def install + bin.install "bin/burn" + end + + test do + assert_match "burn", shell_output("#{bin}/burn --help") + assert_match(/at least \d+/, shell_output("#{bin}/burn 500 2>&1", 2)) + end +end diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..96d1fb5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Daniel Newman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7597925 --- /dev/null +++ b/README.md @@ -0,0 +1,198 @@ +# burn-baby-burn + +> *"Burn, baby, burn — disco inferno."* +>     — The Trammps, 1976, on the subject of API quotas + +A tiny bash CLI that wraps [Claude Code](https://docs.claude.com/claude-code) +and sets your token budget on fire, intentionally, in controlled increments. + +```bash +burn 50000 +``` + +That's it. It spins up Claude Code in non-interactive mode, feeds it a +garbage prompt sized to ~1000 input tokens, and keeps doing that until your +target is met or exceeded. Real token counts come back from the API, so the +total you see is the total that hit your bill. + +No Python. No virtualenvs. One bash file. The way nature intended. + +--- + +## Why would anyone want this? + +Glad you asked. A non-exhaustive list of legitimate reasons to burn tokens +on purpose: + +- **Rate-limit and budget testing.** Verify your alerting fires before + production traffic does. +- **Spend-pacing experiments.** Watch your dashboards in real time while a + known quantity of tokens lands on your account. +- **Reproducible load.** Generate a deterministic amount of Claude Code + usage for CI smoke tests, dashboards, or demos. +- **Stress-testing wrappers.** If you've built tooling around Claude Code, + `burn` is a cheap way to drive sustained-but-bounded traffic through it. +- **Curiosity.** You wanted to know what happens. Now you'll know. + +This tool exists because every other "load test" script eventually does +something accidentally useful. `burn` does nothing useful. It is pure, +honest waste — measured to the token. + +--- + +## Install + +### Homebrew (recommended) + +```bash +brew tap dtnewman/burn https://github.com/dtnewman/burn_baby_burn +brew install burn +``` + +This installs the `burn` script to `$(brew --prefix)/bin/burn` and pulls in +`jq` as a dependency. The formula lives in this repo at `Formula/burn.rb`. + +### One-liner (no brew) + +```bash +curl -fsSL https://raw.githubusercontent.com/dtnewman/burn_baby_burn/main/bin/burn \ + -o /usr/local/bin/burn && chmod +x /usr/local/bin/burn +``` + +You'll also need `jq`: + +```bash +brew install jq # macOS +sudo apt install jq # Debian/Ubuntu +``` + +### From a clone + +```bash +git clone https://github.com/dtnewman/burn_baby_burn +cd burn_baby_burn +ln -s "$PWD/bin/burn" /usr/local/bin/burn +``` + +### Prerequisites + +- [Claude Code](https://docs.claude.com/claude-code) installed and + authenticated on your `PATH`. If `claude --version` works, you're good. +- [`jq`](https://jqlang.github.io/jq/) for parsing Claude Code's JSON + output. `brew install jq` if you don't have it. +- `bash` 3.2+ (i.e. anything from this millennium). + +--- + +## Usage + +``` +burn TOKENS [--model MODEL] +``` + +| Arg / flag | Meaning | +|------------------|----------------------------------------------------------------------| +| `TOKENS` | Target number of tokens to burn. **Minimum: 10000.** | +| `--model MODEL` | Optional. Pass through to `claude --model` (e.g. `haiku`, `sonnet`). | +| `-h`, `--help` | Show help. | + +### Examples + +Burn ten thousand tokens, the absolute minimum: + +```bash +burn 10000 +``` + +Burn fifty thousand tokens on Haiku (cheap and fast — perfect for soak tests): + +```bash +burn 50000 --model haiku +``` + +Burn a hundred thousand and walk away: + +```bash +burn 100000 --model sonnet +``` + +### What you'll see + +``` +$ burn 10000 +burn baby burn — going to try to burn approximately 10000 tokens. +each iteration spins up `claude -p` and burns ~10000 tokens. + +[1] burning… +11142 tokens $0.034230 (total 11142 / 10000, $0.034230) + +done. burned 11142 tokens across 1 call(s) (target was 10000), spent $0.034230. +``` + +A single `claude -p` call carries non-trivial system-prompt overhead, so +the first iteration almost always overshoots. Subsequent iterations are +cheaper (prompt caching kicks in). `burn` always reports the **actual** +token counts and dollar cost returned by the API, summed across input, +cache reads, cache writes, and output. + +--- + +## How it works + +1. Parse the target (must be ≥ 10000 — a single `claude -p` call already + burns about that much, so anything smaller is just a typo). +2. Build a prompt of roughly 1000 input tokens: a "reply with the word + 'burned'" instruction followed by lorem-ipsum-grade disco-inferno filler. +3. Shell out to `claude -p --output-format json --no-session-persistence` + with that prompt. +4. `jq` extracts the `usage` block from Claude Code's JSON response. +5. Tokens burned this call = + `input_tokens + cache_creation_input_tokens + cache_read_input_tokens + output_tokens`. +6. Repeat until the running total meets or exceeds your target. +7. Print the receipts. + +`Ctrl-C` at any time stops the burn and tells you how far you got. + +The whole thing is ~140 lines of bash. You can read it before you run it. +You probably should. + +--- + +## What it does NOT do + +- It does **not** mint tokens, generate value, or accomplish anything. +- It does **not** modify files, run tools, edit your code, or commit + anything. The prompt asks Claude for a one-word reply. +- It does **not** bypass safety, billing, or rate limits. `burn` is just a + shell wrapper; Claude Code does what Claude Code does. +- It does **not** know about your wallet. **You are responsible for the + bill.** Run `burn 100000000` and you'll find out exactly how responsible. + +--- + +## FAQ + +**Q: Why is the minimum 10000?** +A: A single `claude -p` call already burns roughly that much (system-prompt +overhead plus the ~1000-token filler payload). Asking for less is asking +for one call, which the harness can't honestly deliver. + +**Q: Why did `burn 10000` burn 11,142 tokens?** +A: Claude Code attaches a system prompt to every `-p` invocation. That +overhead lands on the first call. `burn` reports honest totals; it does not +fudge them down. + +**Q: Can I make it burn slower?** +A: Pick a smaller, cheaper model (`--model haiku`) and a bigger target. + +**Q: Why bash instead of Python / Go / Rust?** +A: Because it's 140 lines that shell out to another CLI. Anything heavier +would be bringing a flamethrower to a candle. + +**Q: Will Anthropic stop me?** +A: They will charge you. That is how they will stop you. + +--- + +## License + +MIT. Burn responsibly. diff --git a/bin/burn b/bin/burn new file mode 100755 index 0000000..8886f22 --- /dev/null +++ b/bin/burn @@ -0,0 +1,275 @@ +#!/usr/bin/env bash +# burn — a token-burning harness around Claude Code. +# Usage: burn TOKENS [--model MODEL] +set -euo pipefail + +FILLER_TOKENS=1000 +SYSTEM_OVERHEAD_TOKENS=9000 # measured: claude -p system prompt + tool defs +TOKENS_PER_CALL=$(( SYSTEM_OVERHEAD_TOKENS + FILLER_TOKENS )) +MIN_TOKENS=$TOKENS_PER_CALL # asking for less than one call can't be honored +FILLER_LINE="Burn baby burn, this is a disco inferno, burning tokens is for great engineers. " + +usage() { + cat <&2; exit 2; fi + model="$2"; shift 2 ;; + --model=*) model="${1#*=}"; shift ;; + --) shift; break ;; + -*) echo "error: unknown flag: $1" >&2; usage >&2; exit 2 ;; + *) + if [[ -n "$tokens" ]]; then + echo "error: unexpected extra argument: $1" >&2 + exit 2 + fi + tokens="$1"; shift ;; + esac +done + +if [[ -z "$tokens" ]]; then + usage >&2; exit 2 +fi +if ! [[ "$tokens" =~ ^[0-9]+$ ]]; then + echo "error: TOKENS must be a positive integer (got '$tokens')" >&2; exit 2 +fi +if (( tokens < MIN_TOKENS )); then + echo "error: tokens must be at least ${MIN_TOKENS} (got ${tokens})" >&2; exit 2 +fi + +if ! command -v claude >/dev/null 2>&1; then + echo "error: 'claude' CLI not found on PATH." >&2 + echo " install Claude Code first: https://docs.claude.com/claude-code" >&2 + exit 1 +fi +if ! command -v jq >/dev/null 2>&1; then + echo "error: 'jq' not found on PATH (used to parse Claude Code's JSON output)." >&2 + echo " install with: brew install jq" >&2 + exit 1 +fi + +# ~4 chars/token is the standard rule of thumb for English text. +target_chars=$(( FILLER_TOKENS * 4 )) +filler_len=${#FILLER_LINE} +repeats=$(( target_chars / filler_len )) +(( repeats < 1 )) && repeats=1 +filler="" +for ((i=0; i 100 )) && pct=100 + filled=$(( pct * BAR_WIDTH / 100 )) + empty=$(( BAR_WIDTH - filled )) + for ((i=0; i/dev/null || true + wait "$claude_pid" 2>/dev/null || true + fi + echo + printf "stopped. burned %s tokens across %s call(s), spent \$%s.\n" \ + "$burned" "$iteration" "$total_cost" + exit 130 +} +trap on_interrupt INT + +# Run one claude call in the background while animating flames. On success +# leaves the parsed stdout in $last_output. Returns the claude exit status. +run_claude() { + local output_file + output_file=$(mktemp) + "${claude_cmd[@]}" "$prompt" > "$output_file" 2>/dev/null & + claude_pid=$! + + local frame=0 + while kill -0 "$claude_pid" 2>/dev/null; do + redraw_flames $(( frame % 3 )) + frame=$(( frame + 1 )) + sleep 0.25 + done + + if wait "$claude_pid" 2>/dev/null; then + last_output=$(<"$output_file") + rm -f "$output_file" + claude_pid="" + return 0 + else + rm -f "$output_file" + claude_pid="" + return 1 + fi +} + +printf '%s' "$HIDE_CURSOR" +echo +draw_fireplace 0 +echo +draw_bar + +while (( burned < tokens )); do + iteration=$(( iteration + 1 )) + + if ! run_claude; then + draw_bar " (retrying)" + if ! run_claude; then + printf "\nerror: two consecutive claude calls failed; aborting.\n" >&2 + exit 1 + fi + fi + + read -r this_call this_cost < <(jq -r ' + [ ((.usage.input_tokens // 0) + + (.usage.cache_creation_input_tokens // 0) + + (.usage.cache_read_input_tokens // 0) + + (.usage.output_tokens // 0)), + (.total_cost_usd // 0) + ] | @tsv' <<<"$last_output") + + if ! [[ "$this_call" =~ ^[0-9]+$ ]]; then + printf "\nerror: could not parse token usage from claude output\n" >&2 + exit 1 + fi + + burned=$(( burned + this_call )) + total_cost=$(awk -v a="$total_cost" -v b="$this_cost" 'BEGIN{printf "%.6f", a+b}') + draw_bar +done + +echo +printf "done. burned %s tokens across %s call(s) (target was %s), spent \$%s.\n" \ + "$burned" "$iteration" "$tokens" "$total_cost"