{"id":51956196,"url":"https://github.com/izeigerman/claude-thermos","last_synced_at":"2026-08-04T23:00:26.094Z","repository":{"id":373018628,"uuid":"1307025984","full_name":"izeigerman/claude-thermos","owner":"izeigerman","description":"Keeps your Claude session warm for you","archived":false,"fork":false,"pushed_at":"2026-07-24T20:20:26.000Z","size":18462,"stargazers_count":129,"open_issues_count":0,"forks_count":5,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-07-24T22:09:00.255Z","etag":null,"topics":["ai-agents","claude-code"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/izeigerman.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-07-20T21:22:49.000Z","updated_at":"2026-07-24T22:03:36.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/izeigerman/claude-thermos","commit_stats":null,"previous_names":["izeigerman/claude-thermos"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/izeigerman/claude-thermos","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izeigerman%2Fclaude-thermos","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izeigerman%2Fclaude-thermos/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izeigerman%2Fclaude-thermos/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izeigerman%2Fclaude-thermos/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/izeigerman","download_url":"https://codeload.github.com/izeigerman/claude-thermos/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izeigerman%2Fclaude-thermos/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":36291062,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-08-04T02:00:06.901Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-agents","claude-code"],"created_at":"2026-07-29T11:00:30.095Z","updated_at":"2026-08-04T23:00:26.086Z","avatar_url":"https://github.com/izeigerman.png","language":"Python","funding_links":[],"categories":["Python","🛠️ Tools \u0026 Utilities"],"sub_categories":[],"readme":"# claude-thermos\n\n**Stop paying to rebuild your Claude Code cache.** When your main agent waits on a subagent for more than 5 minutes, its prompt cache silently expires, and the next turn re-encodes your entire conversation at the write rate instead of reading it back cheap. On long sessions with many subagents that's roughly 20% of your bill. `claude-thermos` keeps the cache warm so you never pay that tax.\n\n## Use\n\nRun Claude Code exactly as you normally would, but through `claude-thermos` with `uvx`:\n\n```bash\nuvx claude-thermos                     # instead of: claude\nuvx claude-thermos -p \"fix the bug\"    # any claude args pass straight through\n```\n\nRequires Python 3.11+ and the `claude` CLI on your `PATH`.\n\nThat's it. Warming runs automatically in the background. To disable it for a run without changing the command, set `CLAUDE_THERMOS_DISABLE=1`.\n\nTuning (all optional):\n\n| Flag                | Default | Meaning                                                     |\n|---------------------|---------|-------------------------------------------------------------|\n| `--idle`            | `270`   | Seconds the main agent must be idle before warming kicks in |\n| `--interval`        | `270`   | Seconds between warming cycles                              |\n| `--max-cycles`      | `4`     | Max warms per idle episode (`auto` for unlimited)           |\n| `--subagent-window` | `540`   | Seconds a subagent counts as \"still active\"                 |\n\n### Choosing which `claude` to run\n\nBy default `claude-thermos` launches the `claude` found on your `PATH`. Point\nit at a different binary with `--bin`, or the `CLAUDE_THERMOS_BIN` environment\nvariable. A bare name is looked up on `PATH` (so `--bin claude-nightly`\nworks); a full path is used as-is. This is handy for a vendored build or a\nwrapper that exports a different `CLAUDE_CONFIG_DIR` per account.\n\n```bash\nclaude-thermos --bin /path/to/bin/claude -p \"fix the bug\"\n# or\nexport CLAUDE_THERMOS_BIN=/path/to/bin/claude\nclaude-thermos -p \"fix the bug\"\n```\n\nThe flag must come before any passthrough `claude` args.\n\n## Daemon mode (shared proxy for the IDE and multiple terminals)\n\nThe default command warms only the `claude` process it launches. Clients that\nlaunch `claude` themselves — the **VSCode/Claude Code extension**, which spawns\nits own bundled binary — never go through it, and neither do other terminals.\n\n`claude-thermos serve` runs the warming proxy as a **standalone daemon** on a\nfixed loopback port. Point any client at it and they all share one warmer:\n\n```bash\nclaude-thermos serve --port 8787          # run the daemon (Ctrl-C / SIGTERM to stop)\n\n# then, for any client:\nexport ANTHROPIC_BASE_URL=http://127.0.0.1:8787\nclaude -p \"fix the bug\"                    # terminal — warmed by the daemon\n```\n\nFor the VSCode extension, make sure its process inherits that environment\nvariable (on macOS, `launchctl setenv ANTHROPIC_BASE_URL http://127.0.0.1:8787`\nbefore launching the app; or export it in the shell you start the editor from).\nThe extension honors `ANTHROPIC_BASE_URL`, so its traffic then flows through the\ndaemon and its main agent stays warm while subagents run.\n\nThe daemon observes traffic exactly like the launcher and already tracks many\nsessions at once, so a single daemon serves every client on the machine. It\nevicts sessions idle longer than `--session-ttl` (default `3600s`) so it can run\nindefinitely.\n\nTuning: `serve` accepts the same `--idle/--interval/--max-cycles/--subagent-window`\nflags as the default command, plus:\n\n| Flag            | Default                     | Meaning                                       |\n|-----------------|-----------------------------|-----------------------------------------------|\n| `--port`        | `8787`                      | Loopback port the daemon listens on           |\n| `--upstream`    | `https://api.anthropic.com` | Real API the proxy reverse-proxies to         |\n| `--session-ttl` | `3600`                      | Seconds a session may sit idle before eviction|\n\n\u003e **Caveat:** `--upstream` must be the real API, never the daemon's own loopback\n\u003e address — otherwise the proxy would forward to itself. `serve` rejects a\n\u003e loopback upstream, so if you export `ANTHROPIC_BASE_URL` globally, still start\n\u003e the daemon with an explicit `--upstream https://api.anthropic.com`.\n\n## Why your cache keeps expiring\n\nClaude Code's prompt cache uses a **5-minute TTL**. Every turn, your whole conversation history is served from cache at **0.1x** the input price instead of being re-sent at full price, as long as the cache stays alive.\n\nThe cache expires if more than 5 minutes pass between requests on the same prefix. The dominant trigger for that gap is not you thinking. It's the main agent **blocked on a subagent that runs longer than 5 minutes**. A subagent has a different system prompt and tool set, so its requests have a *different* cache prefix and never refresh the main agent's. While the subagent works, the main agent's cached history ages untouched; past 5 minutes it's gone. When the subagent returns, the main agent resumes with a byte-identical, append-only history, and finds its cache missing, forcing a full re-encode at the **1.25x** write rate.\n\nBy then the history is large, so the re-encode is expensive: individual collapses re-write 200K to 500K tokens. Measured across roughly 185 local sessions, these rebuilds accounted for about **22% of the total bill**, money spent re-encoding content that was already cached moments earlier.\n\n## How it works\n\n`claude-thermos` launches Claude Code behind a small local reverse proxy (it points `ANTHROPIC_BASE_URL` at a loopback port; all traffic still goes to the real Anthropic API).\n\n1. **Observe.** The proxy watches `/v1/messages` traffic and groups it into sessions and *lineages*, a lineage being one cache prefix, keyed by model + tool set + system text. The first tool-bearing lineage is the **main** agent; the rest are subagents.\n2. **Detect the danger window.** When the main lineage goes idle *and* a subagent is actively running, the main prefix is at risk of expiring.\n3. **Warm.** On an interval under the 5-minute TTL, it replays the main agent's last real request as a **warm request**: identical cacheable prefix, but `max_tokens: 1` and no streaming. The single token is thrown away; the point is the prefill, which reads and refreshes the full cached prefix. Warm requests go **directly** to the API, never through the proxy, so they can't disturb real traffic.\n4. **Result.** When the subagent finishes, the main agent's cache is still warm. It pays a cheap read instead of a full rewrite.\n\nEach warm costs a cache read (0.1x); each rewrite it prevents would have cost a write (1.25x) on a much larger prefix, so the trade is heavily in your favor.\n\n## Event logs \u0026 savings\n\nEvery session writes to:\n\n```\n~/.claude-thermos/logs/\u003csession_id\u003e/\n├── events.jsonl    # append-only structured event stream\n└── summary.json    # rollup totals, written when the session ends\n```\n\n`events.jsonl` records each request/response's token usage plus every warming decision (`warm_fired`, `warm_result`, `cap_reached`, `resume_detected`, and so on). `summary.json` is the rollup you'll usually read:\n\n| Field                    | Meaning                                                                                    |\n|--------------------------|--------------------------------------------------------------------------------------------|\n| `warms_fired`            | Warm requests sent                                                                         |\n| `cache_read_total`       | Tokens read back by those warms                                                            |\n| `episodes`               | Idle-with-subagent episodes that ended in a successful resume (a rewrite actually avoided) |\n| `rewrite_avoided_tokens` | Tokens that *would have* been re-written, summed across episodes                           |\n| `warm_cost`              | What warming cost you: `0.1 × cache_read_total`                                            |\n| `rewrite_avoided_cost`   | What it saved: `1.25 × rewrite_avoided_tokens`                                             |\n| `net_savings`            | `rewrite_avoided_cost − warm_cost`                                                         |\n\nAll three cost figures are in **base-input-token units** (token counts already weighted by their cache multiplier). To turn `net_savings` into dollars, **multiply it by your model's price per input token**:\n\n```\ndollars saved ≈ net_savings × (input token price)\n```\n\nFor example, at an input price of $3 / 1M tokens, a `net_savings` of `1_200_000` is about `1_200_000 × $3 / 1_000_000 = $3.60` saved that session.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fizeigerman%2Fclaude-thermos","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fizeigerman%2Fclaude-thermos","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fizeigerman%2Fclaude-thermos/lists"}