Claude Codeのトークン消費を削る実践 — 計測してから削った5箇所 #Python - Qiita
@joinclass(Agun Kyo)
Reducing Claude Code token consumption in practice — five areas I trimmed after measuring
  • ClaudeCode
  • AI
  • automation
  • Bash
  • Python
0
Posted at 2026-11-10

Introduction: The cost limit guard had never once been triggered

Our company (Godo Kaisha Join Class, where I am the sole employee) runs `claude -p` unattended from scheduled launchd jobs to organize departmental status, supplement article ideas, collect leads, and perform similar tasks.

We had also set up a system with a monthly limit of $200 that would send a warning to Slack at $160. I had felt reassured, but when I checked its internals on September 25, 2026, the guard had never once been triggered.

The cause was simple: the cost-recording function merely added the “approximate value passed by the caller.” The morning digest script always passed a fixed 0.02. In other words, it was checking the limit against a fictitious number.

In this article, I will first explain how we made actual measurement possible, and then introduce the five areas we trimmed while measuring actual usage, together with the code that is actually running.

Prerequisite: Stop using estimates and record total_cost_usd

When you run `claude --output-format json`, the response JSON contains `total_cost_usd` and `usage`. Simply totaling these values gives you actual measurements.

Our implementation consists of two files. First, the wrapper:

.company/scripts/lib/claude-run.sh
#!/bin/bash # Common wrapper for running claude unattended. Records actual costs. # Usage: # source .company/scripts/lib/claude-run.sh # RESULT=$(claude_run dept-dev --effort low -p "…") ## Outputs only the response text to stdout, so it can replace the existing `claude -p`. # Note: --bare does not read authentication, so it fails with "Not logged in". Do not use it._CLAUDE_RUN_LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" claude_run() { local label="$1"; shift claude --output-format json \ --fallback-model claude-sonnet-5 \ --max-budget-usd 3 \ "$@" 2>/dev/null \ | python3 "$_CLAUDE_RUN_LIB/record-cost.py" "$label" }

The point is that only the response text is streamed to standard output. Since all you need to do is replace the existing RESULT=$(claude -p "...") with RESULT=$(claude_run label -p "..."), we were able to migrate the dozen-odd scripts all at once.

Next is the part that receives the JSON and records it (only the key points are excerpted).

.company/scripts/lib/record-cost.py
def main(): label = sys.argv[1] raw = sys.stdin.read() try: d = json.loads(raw) except json.JSONDecodeError: sys.stdout.write(raw) # If it's not JSON, pass it through as-is return 0 sys.stdout.write(d.get("result") or "")path = pathlib.Path(os.environ.get("COST_FILE") or DEFAULT_COST_FILE) month = datetime.date.today().strftime("%Y-%m") try: t = json.loads(path.read_text(encoding="utf-8")) except Exception: t = {} if t.get("month") != month: # When the month changes, set aside the previous month's data and reopen the file if t.get("month"): path.with_name(f"cost-tracker-{t['month']}.json").write_text( json.dumps(t, ensure_ascii=False, indent=1), encoding="utf-8") t = {"month": month} cost = float(d.get("total_cost_usd") or 0)usage = d.get("usage") or {} before = float(t.get("total_usd") or 0) t["total_usd"] = round(before + cost, 6) jobs = t.setdefault("jobs", {}) j = jobs.setdefault(label, {"calls": 0, "usd": 0.0, "errors": 0, "cache_creation_tokens": 0}) j["calls"] += 1 j["usd"] = round(j["usd"] + cost, 6)j["cache_creation_tokens"] = j.get("cache_creation_tokens", 0) + int( usage.get("cache_creation_input_tokens") or 0) if d.get("is_error"): j["errors"] = j.get("errors", 0) + 1 if d.get("is_error"): j["errors"] = j.get("errors", 0) + 1 path.write_text(json.dumps(t, ensure_ascii=False, indent=1), encoding="utf-8") # Notify only when the threshold is crossed (if it fires every time, it will start being ignored) now = t["total_usd"] if before < STOP_USD <= now: notify_slack(f":rotating_light: Monthly AI spending has reached ${now:.2f}") elif before < WARN_USD <= now:notify_slack(f":warning: Monthly AI spend ${now:.2f} / ${STOP_USD:.0f}")

Limiting the data retained for each job to cache_creation_tokens worked. The actual stored data looks like this (excerpt as of November 10, 2026).

cost-tracker.json
{ "month": "2026-11", "total_usd": 30.3496, "jobs": {"dept-dev": { "calls": 10, "usd": 3.505745, "cache_creation_tokens": 362369 }, "sme-concerns": { "calls": 10, "usd": 6.30988, "cache_creation_tokens": 620803 },"replenish": { "calls": 25, "usd": 12.151766, "cache_creation_tokens": 1041967 } } }

Pitfalls

  • Debug by removing 2>/dev/null: Errors such as an expired login are output to stderr. Immediately after the migration, remove it and run the command manually once.
  • Do not use --bare: We tried it to make startup lighter, but it failed with "Not logged in" because it did not read the authentication information.
  • Pass through non-JSON output as-is: If you do not ensure that the response is not discarded when parsing fails, subsequent processing will run with an empty string.

What we learned from measuring: the fixed cost per call is high

The first thing that surprised us when we started measuring was thatsimply asking "What is 2+2?" cost $0.27–0.47 per call.

Looking at the breakdown, cache creation was about 41,000 tokens. Of that, our own files (CLAUDE.md, agent definitions, and skills) accounted for only about 5,800 tokens. The rest is fixed overhead on the Claude Code side.

In other words, the idea that "all you need to do is shorten CLAUDE.md" has only a limited effect, whilewhat really matters is the number of calls and what you have it do in each call. The following five areas have been trimmed based on that premise.

The five areas we trimmed

1. Set routine jobs to --effort low

Routine work such as summarizing the status of each department does not require deep reasoning.

auto-dept-dev.sh
source "$SCRIPT_DIR/lib/claude-run.sh" RESULT=$(claude_run dept-dev --effort low -p " ...(instructions to read STATE.md and summarize the differences) ")

In our case, only one job that does not have --effort low specified remains, and comparing them shows a difference.

Job effort Per run
dept-dev low about $0.35
dept-sales low about $0.32
sme-concerns not specified about $0.63

Since the contents of the prompts differ, this is not a pure comparison. That said, the cache-creation tokens are also about 1.7 times higher, and there are signs that tool calls have increased and the number of round trips has grown. The verification method is simple: run the same job several times with and without --effort low, and compare jobs.<label>.usd / calls.The verification method is simple: run the same job several times each with and without --effort low, and compare jobs.<label>.usd / calls.

2. Narrow down the tools for each stage with --allowedTools

Article topic replenishment (replenish) is the job with the most calls. We are splitting the tools that are passed between the research stage and the writing stage.

auto-replenish-articles.sh
# Research stage: read only claude_run replenish --effort low --allowedTools "Read,WebSearch,WebFetch" -p "..." # Writing stage: no web search neededclaude_run replenish --effort low --allowedTools "Write,Read,Edit" -p "..."

When a “search just to be safe” begins while writing, the search results go straight into the context, increasing the token count. Without tools, it cannot take a detour. From a security standpoint, it is also good not to grant unnecessary permissions during unattended execution.

3. Stop a runaway execution with --max-budget-usd 3

The --max-budget-usd 3 set in the wrapper is the maximum amount that can be used in a single execution. With unattended jobs, the frightening thing is not the average, but a single run caught in a loop. With only a monthly cap guard, by the time you notice, a full day’s budget has been spent.

The value is set at more than five times the usual per-run cost ($0.3–$0.6). If it is set too tight, a different kind of incident occurs: normal processing is cut off midway, leaving the article only half-written.

4. Check the monthly limit before execution

Merely recording the figures does not stop anything. At the beginning of each script, check the actual measured value and stop if the limit is exceeded.

common.sh
check_cost_limit() { local tracker="$COMPANY_DIR/scripts/cost-tracker.json" [ -f "$tracker" ] || return 0 local total=$(jq '.total_usd' "$tracker")if (( $(echo "$total >= 200" | bc -l) )); then log_error "Monthly cost limit reached: \$${total}. Abort execution." return 1 fi } # At the start of each job check_cost_limit || exit 0

The function itself had been there all along. The only thing that changed was that the numbers it reads are now real. The "guard wasn't working" issue I wrote about initially has finally been resolved here.

5. Reduce the call count itself

If the fixed cost is approximately 41,000 tokens per call, combining 10 calls into one is the most effective approach. At our company, we reviewed this in the following order.

  1. Sort jobs in descending order of usd
  2. For the top jobs, check whether they "need to be called every time" and whether they "are not being called even though the inputs have not changed since the previous run"
  3. If nothing has changed, finish without calling claude

This is the only command needed to display the top entries.

jq -r '.jobs | to_entries | sort_by(-.value.usd)[] | "\(.value.usd | tostring | .[0:6])\t\(.value.calls) times\t\(.key)"' \ .company/scripts/cost-tracker.json

In November, replenish (25 calls, approximately $12) accounted for 40% of our total. This is where we will make the next change.

Results

The actual monthly figures are as follows.

Month Measured total Notes
2026-09 $26.41 Measurement began on 9/25 (about 6 days’ worth)
2026-10 $128.96 Measured for a full month
2026-11 $30.35 As of the 10th

To be honest, September had a short measurement period, so I could not make a clean comparison “before trimming.” Even so, compared with $128.96 in October, November was at $30.35 as of the 10th (around $90 per month at this pace). Above all, the biggest achievement was that the $160 warning and $200 cutoff guards began working with actual figures.

Summary: Tasks for Today

  1. Change claude -p to --output-format json and record total_cost_usd for each job
  2. After collecting data for a week, look at jobs with the highest usd values
  3. For routine jobs, use --effort low and restrict --allowedTools for each task
  4. Use --max-budget-usd to set a per-run limit and check_cost_limit to set a monthly limit
  5. Finally, reduce the number of calls themselves

The order matters. Trimming CLAUDE.md before measuring had only a slightly more than 10% overall effect in our company.

From a management perspective, what is frightening about AI costs is not that they are high, but that they are unknown. When you can see the numbers, you can make decisions. Through this work, I once again realized that “costs can become one-tenth, but revenue does not become ten times.” I am redirecting the saved money and time to work that directly contributes to revenue.

Recommended Books

I am writing a book that systematically covers the design of CLAUDE.md, configuration, and cost management.

  • 『CLAUDE.md Design Patterns』
  • For unattended execution with launchd / cron, see 『The Complete Guide to Automating Claude Code』

Book list: https://zenn.dev/joinclass?tab=books

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
Sign upLogin