{"slug": "and-in-my-claude-code-status-line-now-watching-fable", "title": "🐢 and 🐇 in My Claude Code Status Line: Now Watching Fable", "summary": "A developer extended their Claude Code status line to track per-model weekly quota buckets after discovering that the tool's built-in 7-day indicator only reflects the all-models limit, allowing Fable usage to hit 100% while the display showed 47%. The updated setup uses two scripts — statusline.sh and a new usage-fetch.sh — that poll Anthropic's undocumented OAuth usage endpoint every five minutes and cache per-model percentages for display, after the developer incurred roughly $200 in extra usage charges.", "body_md": "My Claude Code status line races a tortoise against a hare to show whether I'm burning quota faster than a steady pace. [Last time](https://dev.to/suruseas/and-in-my-claude-code-status-line-now-watching-the-week-1m1e) I switched the main bar to the 7-day window.\n\nI'd kept Fable away from long-running agents — it goes through tokens fast. Then Fable 5.1 landed. [The announcement](https://www.anthropic.com/claude-fable-and-mythos-5-1) said it's \"more comfortable with long, unattended work than Fable 5,\" and estimated it would cost about 25% less than Fable 5 for typical workloads — up to about 45% less for highly agentic work. I wanted to see how it would hold up, so I left agents running on it. The status line said the week was fine, and I found out afterwards that I'd gone through Fable's own weekly limit and into extra usage — about $200 of it. I hadn't known that Fable's weekly limit could also be extended with extra usage.\n\nThe status line wasn't wrong. It was watching the wrong bucket.\n\nBesides the all-models 7-day window, there are per-model weekly limits. `/usage` shows them, but the status line payload doesn't carry them: `rate_limits.seven_day` is the all-models bucket, even while you're on Fable. So `7d:47%` can look perfectly relaxed while Fable is at 100%.\n\nNow the line looks like this:\n\n```\n[Opus 5] ·········🐇🐢········· 7d(all):47%@9/16 | ·🐇·🐢······ 5h:15%@14:50 | Fable 7d:87% ⚠️\n```\n\n`7d(all)` — renamed from `7d`, so it's obvious it's not model-specific.` Fable 7d:87% ⚠️` — the per-model weekly bucket.\nWhich per-model buckets get shown:\n\n`Fable 7d:…`.\n⚠️ on a per-model segment uses the same tortoise rule as the bars: usage percentage > elapsed fraction of the window. Unlike the bars, it's suppressed below 25%, so a fresh window doesn't flag on the first few messages.\n\n📦 **The full code is in [this gist](https://gist.github.com/suruseas/49ba6edfb2b8daf94cde32fd2a6b59c0)** — both scripts, the settings snippet, and a README. The sections below walk through it.\n\nThe per-model numbers come from `https://api.anthropic.com/api/oauth/usage` — the endpoint `/usage` reads — authenticated with the Claude Code OAuth token. **It's undocumented, so use this at your own risk.** More on that at the end.\n\nThis version is **two scripts** instead of one. Both live in `~/.claude/`:\n\n```\n~/.claude/\n├── settings.json       # statusLine points at statusline.sh only\n├── statusline.sh       # updated — draws the line, starts usage-fetch.sh\n├── usage-fetch.sh      # new — calls the usage endpoint, writes the cache\n└── usage-cache.json    # written by usage-fetch.sh, read by statusline.sh\n```\n\n`settings.json` only knows about `statusline.sh`. You never run `usage-fetch.sh` yourself — `statusline.sh` starts it in the background at most once every 5 minutes, and reads whatever it last cached:\n\n```\non every render : Claude Code → statusline.sh → reads usage-cache.json → prints the line\nevery 5 minutes : statusline.sh → starts usage-fetch.sh in the background\nin background   : usage-fetch.sh → calls the usage endpoint → writes usage-cache.json\n```\n\nThe relevant part of the response:\n\n```\n{\n  \"limits\": [\n    {\n      \"kind\": \"weekly_scoped\",\n      \"scope\": { \"model\": { \"display_name\": \"Fable\" } },\n      \"percent\": 0.87,\n      \"resets_at\": \"2026-09-16T09:00:00.126747+00:00\"\n    }\n  ]\n}\n```\n\nThe script keeps only the per-model weekly buckets and caches them:\n\n```\n{\"fetched_at\":1789266899,\"buckets\":[{\"name\":\"Fable\",\"pct\":87,\"resets_at\":1789549199}]}\nbash\n#!/bin/bash\n# Fetch per-model weekly quota buckets and cache them for statusline.sh.\nset -u\n\nCACHE=~/.claude/usage-cache.json\nLOCK=~/.claude/.usage-fetch.lock\nATTEMPT=~/.claude/.usage-fetch-attempt\n\n# Single flight. A lock older than 2min is stale (killed mid-fetch).\nif [ -d \"$LOCK\" ]; then\n    [ -n \"$(find \"$LOCK\" -maxdepth 0 -mmin +2 2>/dev/null)\" ] && rmdir \"$LOCK\" 2>/dev/null\nfi\nmkdir \"$LOCK\" 2>/dev/null || exit 0\ntrap 'rmdir \"$LOCK\" 2>/dev/null' EXIT\n\n# Record the attempt first, so a failing fetch backs off like a successful one\n# instead of re-firing on every render.\ndate +%s > \"$ATTEMPT\"\n\n# OAuth token: keychain on macOS, credentials file elsewhere. Never written out.\nTOKEN=$(security find-generic-password -s \"Claude Code-credentials\" -w 2>/dev/null \\\n        | jq -r '.claudeAiOauth.accessToken // .accessToken // .access_token // empty' 2>/dev/null)\n[ -n \"${TOKEN:-}\" ] || TOKEN=$(jq -r '.claudeAiOauth.accessToken // .accessToken // .access_token // empty' \\\n        ~/.claude/.credentials.json 2>/dev/null)\n[ -n \"${TOKEN:-}\" ] || exit 0\n\nRESP=$(curl -sS --max-time 8 https://api.anthropic.com/api/oauth/usage \\\n        -H \"Authorization: Bearer $TOKEN\" \\\n        -H \"Content-Type: application/json\" \\\n        -H \"anthropic-beta: oauth-2025-04-20\" 2>/dev/null)\nunset TOKEN\n[ -n \"$RESP\" ] || exit 0\n\n# Keep only the weekly per-model windows. `percent` comes back as a 0-1\n# fraction, but tolerate a real percentage in case that ever changes.\nOUT=$(printf '%s' \"$RESP\" | jq -c --argjson now \"$(date +%s)\" '\n    # resets_at arrives as \"2026-09-16T09:00:00.126747+00:00\" - fractional\n    # seconds and an offset, neither of which jq fromdate accepts.\n    def iso2epoch:\n      if type != \"string\" then null else\n        capture(\"^(?<b>\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2})(\\\\.\\\\d+)?(?<tz>Z|[+-]\\\\d{2}:\\\\d{2})?$\") as $c\n        | if $c == null then null else\n            (($c.b + \"Z\") | fromdateiso8601) as $t\n            | ($c.tz // \"Z\") as $tz\n            | if $tz == \"Z\" then $t\n              else (($tz[1:3] | tonumber) * 3600 + ($tz[4:6] | tonumber) * 60) as $off\n                   | if $tz[0:1] == \"+\" then $t - $off else $t + $off end\n              end\n          end\n      end;\n    {fetched_at: $now,\n     buckets: [ (.limits // [])[]\n       | select(.kind == \"weekly_scoped\" and (.scope.model.display_name | type) == \"string\")\n       | {name: .scope.model.display_name,\n          pct:  (if (.percent // 0) <= 1 then (.percent // 0) * 100 else .percent end),\n          resets_at: (.resets_at | iso2epoch)} ]}\n    | select(.buckets | length > 0)' 2>/dev/null)\n[ -n \"$OUT\" ] || exit 0\n\numask 077\nprintf '%s\\n' \"$OUT\" > \"$CACHE\".tmp && mv -f \"$CACHE\".tmp \"$CACHE\"\n```\n\nA few details:\n\n`~/.claude/.credentials.json` on other platforms), passed to `curl`, and unset. Only percentages and reset times are written to disk, with `umask 077`.` mkdir` as a lock.`resets_at` needs a hand-rolled parser.`fromdateiso8601` rejects both the fractional seconds and the `+00:00` offset.`tmp` + `mv`` statusline.sh` never reads a half-written file.\nEverything up to the 5h bar is the same as the previous post. New is the block at the end and the `7d(all)` label:\n\n``` bash\n#!/bin/bash\ninput=$(cat)\n\nMODEL=$(echo \"$input\" | jq -r '.model.display_name')\nFIVE_H_PCT=$(echo \"$input\" | jq -r '(.rate_limits.five_hour.used_percentage // 0)')\nSEVEN_D_PCT=$(echo \"$input\" | jq -r '(.rate_limits.seven_day.used_percentage // 0)')\nFIVE_H_RESETS=$(echo \"$input\" | jq -r '.rate_limits.five_hour.resets_at // empty')\nSEVEN_D_RESETS=$(echo \"$input\" | jq -r '.rate_limits.seven_day.resets_at // empty')\n\nNOW=$(date +%s)\nTZ=Asia/Tokyo  # change to your local timezone\nW7=20; W5=10   # bar widths: 7d full, 5h half\n\nmake_bar() {\n    local actual=$1 ideal=$2 width=$3 bar=\"\" i\n    for i in $(seq 0 $((width - 1))); do\n        if [ \"$i\" -eq \"$ideal\" ] && [ \"$i\" -eq \"$actual\" ]; then bar=\"${bar}🐢🐇\"\n        elif [ \"$i\" -eq \"$ideal\" ]; then bar=\"${bar}🐢\"\n        elif [ \"$i\" -eq \"$actual\" ]; then bar=\"${bar}🐇\"\n        else bar=\"${bar}·\"\n        fi\n    done\n    [ \"$actual\" -ge \"$width\" ] && bar=\"${bar}🐇\"\n    echo \"$bar\"\n}\n\n# 7-day window (main bar, width=20)\nif [ -n \"$SEVEN_D_RESETS\" ]; then\n    REMAINING_7D=$((SEVEN_D_RESETS - NOW))\n    RESET_7D_MD=$(date -r \"$SEVEN_D_RESETS\" \"+%m/%d\" | awk -F/ '{printf \"%d/%d\", $1, $2}')\n    RESET_7D_TAG=\"@${RESET_7D_MD}\"\n    if [ \"$REMAINING_7D\" -gt 0 ] && [ \"$REMAINING_7D\" -lt 604800 ]; then\n        IDEAL_7D=$(( (604800 - REMAINING_7D) * W7 / 604800 ))\n    else\n        IDEAL_7D=0\n    fi\nelse\n    IDEAL_7D=0; RESET_7D_TAG=\"@?\"\nfi\nACTUAL_7D=$(awk \"BEGIN {print int($SEVEN_D_PCT * $W7 / 100)}\")\nBAR_7D=$(make_bar \"$ACTUAL_7D\" \"$IDEAL_7D\" \"$W7\")\n[ \"$ACTUAL_7D\" -gt \"$IDEAL_7D\" ] && WARN_7D=\" ⚠️\" || WARN_7D=\"\"\nSEVEN_D_DISP=$(printf \"%.0f\" \"$SEVEN_D_PCT\")\n\n# 5-hour window (half bar, width=10)\nif [ -n \"$FIVE_H_RESETS\" ]; then\n    REMAINING_5H=$((FIVE_H_RESETS - NOW))\n    RESET_5H_JST=$(date -r \"$FIVE_H_RESETS\" \"+%H:%M\")\n    RESET_5H_TAG=\"@${RESET_5H_JST}\"\n    if [ \"$REMAINING_5H\" -gt 0 ] && [ \"$REMAINING_5H\" -lt 18000 ]; then\n        IDEAL_5H=$(( (18000 - REMAINING_5H) * W5 / 18000 ))\n    else\n        IDEAL_5H=0\n    fi\nelse\n    IDEAL_5H=0; RESET_5H_TAG=\"@?\"\nfi\nACTUAL_5H=$(awk \"BEGIN {print int($FIVE_H_PCT * $W5 / 100)}\")\nBAR_5H=$(make_bar \"$ACTUAL_5H\" \"$IDEAL_5H\" \"$W5\")\n[ \"$ACTUAL_5H\" -gt \"$IDEAL_5H\" ] && WARN_5H=\" ⚠️\" || WARN_5H=\"\"\nFIVE_H_DISP=$(printf \"%.0f\" \"$FIVE_H_PCT\")\n\n# Per-model weekly windows. Not in the payload - seven_day is the all-models\n# bucket even while on Fable - so a detached helper caches them from the same\n# endpoint /usage reads. Rendering never waits on it and stays silent on failure.\nUSAGE_CACHE=~/.claude/usage-cache.json\nUSAGE_ATTEMPT=~/.claude/.usage-fetch-attempt\nLAST_ATTEMPT=$(cat \"$USAGE_ATTEMPT\" 2>/dev/null || echo 0)\nif [ $((NOW - LAST_ATTEMPT)) -ge 300 ]; then\n    (nohup ~/.claude/usage-fetch.sh >/dev/null 2>&1 &) 2>/dev/null\nfi\n\n# Current model's own weekly bucket, plus any other bucket already past pace.\nSCOPED=$(jq -r --arg model \"$MODEL\" --argjson now \"$NOW\" '\n    def pace($b): if ($b.resets_at // 0) > $now and ($b.resets_at - $now) < 604800\n                  then (604800 - ($b.resets_at - $now)) * 100 / 604800 else 100 end;\n    def seg($b): \" | \\($b.name) 7d:\\($b.pct | round)%\"\n                 + (if $b.pct > pace($b) and $b.pct >= 25 then \" \\u26a0\\ufe0f\" else \"\" end);\n    (.buckets // []) as $bs\n    | ($model | ascii_downcase) as $m\n    | ($bs | map(select(.name as $n | $m | startswith($n | ascii_downcase))) | first) as $mine\n    | (if $mine then seg($mine) else \"\" end)\n      + ($bs | map(select(.name != ($mine.name // \"\") and .pct > pace(.) and .pct >= 25))\n             | map(seg(.)) | join(\"\"))\n    ' \"$USAGE_CACHE\" 2>/dev/null) || SCOPED=\"\"\n\necho \"[${MODEL}] ${BAR_7D} 7d(all):${SEVEN_D_DISP}%${WARN_7D}${RESET_7D_TAG} | ${BAR_5H} 5h:${FIVE_H_DISP}%${WARN_5H}${RESET_5H_TAG}${SCOPED}\"\n```\n\nNotes on the new block:\n\n`(nohup … &)` in a subshell`Fable 5.1`, the bucket is named `Fable`; `startswith` on lowercased names connects them.`pace()` is the tortoise in percent`SCOPED` is empty and the line looks exactly like the previous version.\nThe scripts are written for macOS: the token is read from the keychain with `security`, and `statusline.sh` uses BSD `date -r <epoch>` (on Linux, use `date -d @<epoch>`). They also need `jq` and `curl`.\n\nPut both scripts in `~/.claude/` and make them executable:\n\n```\nchmod +x ~/.claude/statusline.sh ~/.claude/usage-fetch.sh\n```\n\nIn `settings.json`, the status line still points only at `statusline.sh`. Add `refreshInterval` (why is explained below):\n\n```\n{\n  \"statusLine\": {\n    \"type\": \"command\",\n    \"command\": \"~/.claude/statusline.sh\",\n    \"refreshInterval\": 60\n  }\n}\n```\n\nThis is not a documented API. It's what Claude Code itself calls, it can change without notice, and it isn't clear whether calling it from your own script is an intended use. The script fails silently, so if the endpoint changes or stops answering, the rest of the status line keeps working.\n\nClaude Code doesn't run the status line command on a timer. It runs it on events: at session start, whenever a new assistant message arrives, after `/compact`, when the permission mode changes, and a few others ([docs](https://code.claude.com/docs/en/statusline)). Updates are debounced at 300ms, and if a new update comes in while the script is still running, the running one is cancelled.\n\nWhile an agent is working, that means a render for every message — far too often for an HTTP request. That's why the fetch is a separate script behind the 5-minute check, launched detached so it isn't killed when the next render cancels `statusline.sh`. A new value shows up on the render after the fetch finishes.\n\nWithout `refreshInterval`, there's one request to the usage endpoint roughly every 5 minutes while agents are producing messages, and none when no messages are arriving. The attempt timestamp and the lock are files under `~/.claude/`, so the 5-minute gap is shared across all your Claude Code sessions, not per session.\n\nThe catch is the idle case. When the main session is idle — say, waiting on background subagents — the event triggers go quiet, and the Fable number stops updating while the subagents keep burning it. That's why the setup adds `refreshInterval`: it re-runs the command every N seconds on top of the events, so with `60` the line re-renders every minute. The trade-off is that the request now also happens while you're idle — still at most once every 5 minutes, as long as a Claude Code session is open.", "url": "https://wpnews.pro/news/and-in-my-claude-code-status-line-now-watching-fable", "canonical_source": "https://dev.to/suruseas/and-in-my-claude-code-status-line-now-watching-fable-5fln", "published_at": "2026-09-13 03:41:02+00:00", "updated_at": "2026-09-13 03:56:47.488376+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-products", "large-language-models"], "entities": ["Claude Code", "Anthropic", "Fable", "Fable 5.1", "Opus 5"], "alternates": {"html": "https://wpnews.pro/news/and-in-my-claude-code-status-line-now-watching-fable", "markdown": "https://wpnews.pro/news/and-in-my-claude-code-status-line-now-watching-fable.md", "text": "https://wpnews.pro/news/and-in-my-claude-code-status-line-now-watching-fable.txt", "jsonld": "https://wpnews.pro/news/and-in-my-claude-code-status-line-now-watching-fable.jsonld"}}