
Show HN: Run Claude Code unattended inside a fence, under a dollar budget
SIwithin runs an agent CLI unattended on a brief, inside a fence around the folders you give it and under a dollar budget, and leaves one record per run. It keeps three promises with a tool and not with hope:
以下正文同步自 Hacker News,版权归原站所有,已转换为易读排版。
SIwithin runs an agent CLI unattended on a brief, inside a fence around the folders you give it and under a dollar budget, and leaves one record per run. It keeps three promises with a tool and not with hope:
Works with the Claude Code and OpenAI Codex command-line tools. SIwithin is an independent project; it is not made, endorsed or sponsored by Anthropic or OpenAI. Claude, Claude Code, OpenAI and Codex are the names of their owners, used here only to say which programs SIwithin can drive. You remain bound by each vendor's terms of service and usage policies when you use their CLIs through SIwithin; SIwithin changes none of them.
SIwithin runs on macOS only. On Linux and Windows it refuses to run, in one line, rather than run without a fence.
SIwithin is one file, Python 3.10 or newer, standard library only. It makes no network calls of its own and sends no telemetry; the proxy it starts for a run listens on a loopback port and talks only to the API host you configure.
git clone <this repository> && cd siwithin python3 siwithin.py --version
optional: put it on your PATH
install -m 755 siwithin.py ~/.local/bin/siwithin
You also need the CLI you want to drive, installed and on your PATH, and an API key for it (see "Auth").
The smoke test (python3 tests/smoke.py) passes on the Python it was written on, 3.9.6. 3.10 is the supported floor and the code avoids newer syntax, but it has not been run on 3.10 or newer.
mkdir -p ~/.config/siwithin && echo 20 > ~/.config/siwithin/cap # all runs together may spend $20 export ANTHROPIC_API_KEY=... # see "Auth"; the tool reads it once, outside the fence, and the run gets a per-run token in its place
python3 siwithin.py --dir ~/work/proj --model sonnet --dry-run < brief.md # print everything, run nothing
python3 siwithin.py --dir ~/work/proj --model sonnet --budget 2
--done "ALL TESTS PASS" --out notes.md < brief.md
--dir is the run's folder: the CLI works in it and may write in it (--readonly makes it read-only). --read <folder> adds folders the run may read. --model takes an id or an alias (haiku, sonnet, opus).
mkdir -p ~/.config/siwithin && echo 20 > ~/.config/siwithin/cap export CODEX_API_KEY=...
python3 siwithin.py --adapter codex --model gpt-5.3-codex --soft-budget --network open --dir ~/work/proj --dry-run < brief.md python3 siwithin.py --adapter codex --model gpt-5.3-codex --soft-budget --network open --budget 2 --dir ~/work/proj < brief.md
Two flags are required for this adapter, for reasons in "The Codex adapter": --model (the CLI reports tokens and no cost, so a run is priced by model id) and --soft-budget (the CLI reports usage only when a turn ends). This adapter was built from documentation and has not been run against a live CLI. It runs with auth = "env": the key is in the run's environment (see "Auth"), and since there is no proxy there is no port for the network wall to allow, so it needs --network open, or network = "open" under [fence] in config.toml; under the default the run is refused rather than run with no network (see "The fence").
SIwithin never parses, stores, prints or logs a key, and writes one into no file, log line or error message. How the key reaches the API depends on the adapter's auth field.
The Claude adapter, auth = "proxy" (the default from 0.3.0). Before launch, outside the fence, the runner reads the key once: from ANTHROPIC_API_KEY in its own environment, or else from the first line printed by [auth] key_command in config.toml, a command of yours that prints the key (a keychain lookup, for example). It keeps the key and starts a small HTTP server on a loopback port of its own, standard library only. The run's environment holds ANTHROPIC_API_KEY=siwithin-<32 characters>, a token new for every run and worth nothing elsewhere, and ANTHROPIC_BASE_URL=http://127.0.0.1:<port>. Every request the run makes, by the CLI, by hand or by a second CLI it starts, passes that door: the proxy takes only the run's token (anything else gets 401 and goes nowhere), forwards only the paths on [auth] paths (/v1/messages and /v1/messages/count_tokens by default; any other path gets 403 and the real key is never attached), adds the real key, relays the response as it streams, and prices each /v1/messages response at the [prices] rates from its usage. Once the total reaches the budget every later /v1/messages request gets 402, the first refusal kills the run's processes as the streamed stop does, and the proxy keeps refusing until the runner exits. When the run ends the proxy is sealed, its log closed and its server shut, so the token is worth nothing. One line per request goes to proxy.jsonl in the run's folder (path, model, status, usage, cost, the running total, the client's port, why it was refused; never a body, a key or the token). A settings.json in the run's config directory that names apiKeyHelper is refused before launch, since the CLI would run the helper inside the fence where the run can read it: put the helper's command in [auth] key_command instead, which the runner runs outside the fence. --dry-run prints auth: proxy 127.0.0.1:<port> and starts nothing.
auth = "env" copies the key's variable into the run by name, as 0.2.1 did. It is the Codex adapter's only mode and an opt-in for the Claude adapter (the checks warn). In this mode the key is in the run's environment and inherited by every command the run starts, and the network wall cannot be raised, since there is no proxy port to allow: the run is refused under the default [fence] network = "proxy" and needs network = "open" (or --network open), so the run can send the key anywhere and spend with it outside the ledger. Use a key made for SIwithin alone, in a workspace with a spend limit set in the provider's console, and treat it as exposed to whatever text the run reads (SECURITY.md, "Known gaps").
Each adapter gets a config directory under SIwithin's home (claude-config/, codex-config/), shared by its runs, with no login in it, so a CLI cannot quietly sign with a login and bill that instead of your key.
--require-api-key (on by default for the claude adapter, refused for codex, which has no way to check) asks the CLI before launch how it is signed in, and checks the key source in the CLI's first event, which under the proxy must be ANTHROPIC_API_KEY, the token's variable. If either says a login would be billed, the run is refused or killed, and the message says why: a CLI with a login in its config directory can sign with that login while its events name an API key. --no-require-api-key accepts a login.
SIwithin generates a macOS sandbox-exec profile for each run, from a template, with the home folder derived at run time. --dry-run prints it; each run folder keeps it as fence.sb.
What the fence does not stop: what the run sends to the API itself, inside request bodies the proxy relays unread (the model's context is the one way out the wall leaves), and the whole network under [fence] network = "report" or "open", which an auth = "env" adapter needs; reads outside the home; writes outside the home and the listed roots; whatever the run writes into its own folder and you later run (scripts, git hooks); a key exported in a shell rc file, which the run can read; a process that scrubbed its marker and was re-parented before the walk looked; and anything the CLI does in its own cloud sandbox or on a remote machine, where the fence is not. Read SECURITY.md before relying on it.
Each run appends one line to ledger.md in SIwithin's home and writes a summary line to your terminal:
2026-10-13 14:02 | docs-pass | /Users/you/work/proj | claude sonnet - | 12 turns | 6.4 min | cli $0.4123 | true $0.4123 | in 310k out 18k | runs/2026-10-13-1402-docs-pass | done | receipt 864410d4e9f126c9abf1b0b12c28e21ff8541cf24ceaf98039781609a9133881
cli is the CLI's own cost figure (- when the CLI gives none). true is the cost at published rates from [prices] in config.toml, keyed by model id; with the proxy on, it is the proxy's own total, what left the machine for the API through the one door. [prices] carries the date the rates were read (read_on) and where (source); SIwithin warns when they are over 90 days old. Rates change: check them. Cache writes are priced at the 5-minute rate, and a run that uses a higher tier than the one listed (a long prompt on one model, a long-context or fast tier on another) is understated.
Cap. cap holds one number, the dollars all runs together may spend. SIwithin adds up the true column and refuses a run when the total plus its --budget passes the cap, checked under the lock. cap, --budget and the config's budget refuse nan and inf.
Budget. --budget is true dollars. For a CLI with a budget flag, SIwithin passes budget × factor ([factors] is the ratio between the CLI's units and true dollars, 1 unless listed). The built-in default for the smallest Claude model is 1; CLI 2.1.284 priced it at another model's rates and needed 40, which you can set under [factors] in config.toml if you are on that version. SIwithin warns when the CLI's cost over the true cost is more than four times the factor or less than a quarter of it: the factor has gone stale, in either direction. For a CLI with no budget flag, SIwithin reads usage as it streams and kills the run at the budget. For a CLI that reports usage only when a turn ends, SIwithin refuses unless --soft-budget is given, and then the budget is checked when each turn ends.
Claude Code: the budget is enforced three times. By the CLI's flag (--max-budget-usd, given budget × factor); by SIwithin from the stream (each assistant event carries that response's usage, SIwithin prices it at the [prices] rates, each message id once, and kills the run's processes when the true cost reaches the budget); and by the proxy, which prices every /v1/messages response at the door, refuses with 402 once the total reaches the budget and kills on the first refusal, so a request the CLI's stream does not show is counted too. The ledger line and the summary say killed: budget $0.50 reached at $0.5502 true. When the flag stops the run first, the line says stopped by the CLI's budget flag: Reached maximum budget ($0.10), the CLI's own words, and result.md holds the model's last message, since the CLI's result then carries no text. --dry-run prints all three. The limits, each in the summary notes when it applies:
If the stream carries no usage fields, SIwithin falls back to the flag and the proxy, and the notes say so.
A killed run is booked at the proxy's total where there was a proxy, and otherwise, when its usage was never reported (timer, interrupt), at its budget as a ceiling, its line saying (ceiling, usage unreported), so the cap stays true.
Every run also has a folder under runs/ with brief.md, command.txt (the command, environment variable names and the CLI's version), fence.sb, events.jsonl, stderr.txt, result.md, summary.json, proxy.jsonl on a run that had a proxy, and receipt.json (see "The receipt").
After a run ends, and before its ledger line is appended, the runner writes receipt.json into the run's folder: the sha256 of each file the runner wrote, chained to the previous receipt and to the ledger as it stood. The ledger line then carries the receipt's own hash as its last field, | receipt <64 hex>; the terminal prints it on a line receipt: <64 hex> after session:. The runner writes it after the CLI has exited; the model never does. A run refused before launch writes no receipt and no ledger line, and --dry-run writes nothing. summary.json cannot carry the receipt's hash, because the receipt hashes summary.json.
receipt.json is JSON with sorted keys, indent 1, UTF-8; its hash is the sha256 of its bytes. Fields:
python3 siwithin.py --verify [--home H] # every receipt on the ledger python3 siwithin.py --verify --expect <hash> [--home H] # the same, and the last receipt must be this one python3 siwithin.py --verify ~/.config/siwithin/runs/<run> # one run folder's files against its re
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。