SupaTerra Docs
Everything about the terminal agent: how to install it, how it decides what it may touch, every tool, setting and slash command, and what happens inside a turn. Written from the source, so it matches what the binary actually does.
Introduction
SupaTerra is an AI coding agent that runs in your terminal. You describe a task; it reads your project, edits files, runs commands, and tells you what actually happened.
It talks to Claude through the Anthropic API with your own key. There is no SupaTerra server in between: requests go from your machine straight to api.anthropic.com (or a gateway you choose with baseURL).
- One file to install. Each release is a single executable built with
bun build --compile. No Node, Bun or Python needed. - Asks before it changes anything. Reading is free; writing files and running commands go through a permission gate.
- Seven local tools.
read_file,list_dir,glob,grep,edit_file,write_file,bash, plus optional server-sideweb_search. - Streams everything. Text appears as the model writes it, with one compact line per tool call.
- Scriptable.
supaterra run "…"prints the answer and exits with a meaningful code.
Default model is claude-opus-5-5 at effort high. Both can be changed per run, per session, or permanently. See Models, effort & thinking.
Installation
Download the archive for your platform from the GitHub Releases page, unpack it, and put the executable on your PATH. Every release also ships SHA256SUMS.txt so you can verify the download.
| Platform | Archive | Executable |
|---|---|---|
| windows-x64 | supaterra-windows-x64.zip | supaterra-windows-x64.exe |
| darwin-arm64 | supaterra-darwin-arm64.tar.gz | Apple Silicon Macs |
| darwin-x64 | supaterra-darwin-x64.tar.gz | Intel Macs |
| linux-x64 | supaterra-linux-x64.tar.gz | Most Linux PCs and servers |
| linux-arm64 | supaterra-linux-arm64.tar.gz | Raspberry Pi 4/5, ARM servers |
Install the binary
# unpack and move the exe somewhere on your PATH
Expand-Archive supaterra-windows-x64.zip
Move-Item .\supaterra-windows-x64\supaterra-windows-x64.exe C:\Tools\supaterra.exe
# add C:\Tools to PATH (once), then open a new terminal
supaterra --versionFor the best bash tool experience install Git for Windows. SupaTerra prefers Git Bash and falls back to PowerShell. See bash.
tar -xzf supaterra-darwin-arm64.tar.gz
chmod +x supaterra-darwin-arm64
sudo mv supaterra-darwin-arm64 /usr/local/bin/supaterra
# only if Gatekeeper refuses to open it
xattr -d com.apple.quarantine /usr/local/bin/supaterra
supaterra --versionOn an Intel Mac use the darwin-x64 archive instead.
tar -xzf supaterra-linux-x64.tar.gz
chmod +x supaterra-linux-x64
sudo mv supaterra-linux-x64 /usr/local/bin/supaterra
supaterra --versionOn ARM machines use linux-arm64.
Run from source
You need Bun 1.2 or newer.
git clone <repo-url> SupaTerra
cd SupaTerra
bun install
bun run dev # same as the binary, straight from src/
bun run dev -- --help # pass flags after --To produce your own executables, see Building binaries.
API key
SupaTerra uses your own Anthropic API key. Create one at platform.claude.com, then either store it with login or provide it through the environment.
Store it with login
supaterra login- Paste the key. Input is hidden while you type.
- It is saved as
apiKeyin~/.supaterra/config.json. Other settings in that file are kept. - SupaTerra checks the key by listing one model. If the check fails you see the API error and the command exits with code 1. The key stays saved, so run
loginagain to replace it.
A key that does not start with sk-ant- is saved anyway, with a warning. login needs an interactive terminal; in CI use the environment instead.
Use the environment
export SUPATERRA_API_KEY=sk-ant-... # wins over everything
# or the variable the Anthropic SDK already knows
export ANTHROPIC_API_KEY=sk-ant-...The key is looked up in this order: SUPATERRA_API_KEY, then ANTHROPIC_API_KEY, then apiKey in the config file. If none is set, SupaTerra still tries the credentials the Anthropic SDK can find on its own (for example ANTHROPIC_AUTH_TOKEN). If that also fails, an interactive terminal walks you through login; a non-interactive one exits with No API key. Set SUPATERRA_API_KEY or run `supaterra login`.
The config file stores the key unencrypted. On a shared machine, restrict it (chmod 600 ~/.supaterra/config.json) or use the environment variable. More in Keys & local data.
Quickstart
- Save your key
Run
supaterra loginand paste the key. - Open a project
The folder you start in becomes the agent's working directory.
cd ~/projects/shop-api supaterra - Describe the task
Write it the way you would to a colleague, in any language. The agent answers in the language you use.
❯ parsePrice in src/utils.ts returns NaN for "€1.299,99". fix it and add tests - Approve what it wants to change
Before an edit or a command you get a prompt. Press y to allow once, n to refuse, a to allow that tool for the rest of the session.
⚠ edit_file wants to modify files: edit src/utils.ts [y] yes [n] no [a] always (edit_file, this session) › - Read the result
The agent runs your tests through
bashand reports the real outcome, including failures. A gray line shows the turn's duration and output tokens so far.
Next: learn the slash commands, or put house rules for the agent in a SUPATERRA.md.
Interactive sessions
Running supaterra with no command opens a session in the current directory. The banner shows the version, model, effort and working directory, and whether write and shell calls will ask for permission.
| Key | What it does |
|---|---|
| Enter | Send the message. |
line ending in \ | Continue on the next line. The prompt changes to … until you send a line without a trailing backslash. |
| Ctrl C during a reply | Cancel the response. Running commands are killed and pending tool calls are reported to the model as cancelled. |
| Ctrl C at the prompt | Quit (exit code 130). |
Ctrl D or /exit | Quit. |
| ↑ ↓ | Walk through the last 500 inputs. |
Reading the output
- Text streams in as markdown rendered for the terminal: headings, lists, quotes, fenced code with a language label, inline
code, bold, italic, strikethrough and links. - Each tool call is one line:
⚙ grep /parsePrice/ in *.ts, followed by✓ 4 linesor a red✗with the first line of the error. Calls slower than a second show their duration. - A spinner on stderr shows what is happening: thinking, preparing edit_file (1,240 chars), running bash, searching the web.
- With
--thinking(or/thinking) the model's summarized reasoning appears dimmed, prefixed with┆. - With
--verbosethe first 20 lines of every successful tool result are printed under its summary line.
One-shot & scripting
supaterra run runs a single prompt to completion, prints the answer, and exits. The run keyword is optional: any words that are not a known command are treated as the prompt.
supaterra run "explain the structure of this repo"
supaterra "explain the structure of this repo" # same thing
echo "summarize README.md" | supaterra run # prompt from stdin
supaterra run -y "add unit tests for src/utils.ts and run them"
supaterra run -C ../api -m claude-sonnet-5-5 -e medium "list the HTTP routes"Non-interactive runs
A run is interactive only when both stdin and stdout are terminals. Piping input in or redirecting output (> out.md) makes it non-interactive, and then nobody can answer a permission prompt:
- Read-only tools still run.
edit_file,write_fileandbashare denied, and the model is told so. A gray notice on stderr says so up front.- Pass
--yoloto allow them. Writes outside the working directory are still denied, because they always need a human to say yes.
The answer goes to stdout; the spinner, notices and the final usage line go to stderr. That keeps supaterra run "…" > answer.md clean.
Exit codes
| Code | Meaning |
|---|---|
| 0 | The turn finished normally. |
| 1 | The turn stopped early (cancelled, refused, cut off at max_tokens, hit maxIterations, context still too long), an API error happened, or no API key was available. |
| 2 | Usage error: unknown flag, missing prompt, invalid effort, or --cwd that is not a directory. |
| 130 | Interactive session ended with Ctrl C at the prompt. |
Example: GitHub Actions step
- name: Ask SupaTerra for a changelog entry
env:
SUPATERRA_API_KEY: ${{ secrets.SUPATERRA_API_KEY }}
run: supaterra run -e medium "write a one-paragraph changelog entry for the last commit" > entry.mdSlash commands
Inside a session, a message that starts with / is a command for SupaTerra, not for the model. Changes made with commands last for the session only; make them permanent with supaterra config set.
| Command | What it does |
|---|---|
| /help, /? | List the commands. |
| /clear | Start a new conversation with a new session id. Settings are kept. |
| /compact | Replace the conversation with a model-written summary to free up context. See Context & compaction. |
| /cost | Requests, input, output and cache tokens for this session, with an estimated cost. |
| /model [id] | Show the model, or switch to another model id. |
| /effort [level] | Show or set effort: low, medium, high, xhigh, max. |
| /thinking | Toggle showing summarized thinking. |
| /yolo | Toggle auto-approve for edit_file, write_file and bash. |
| /web | Toggle the server-side web_search tool. |
| /cwd [path] | Show or change the working directory. Relative paths resolve from the current one; project instructions are re-read. |
| /tools | List the tools with their permission level. |
| /config | Print the effective configuration (the key is masked). |
| /save | Save this session to disk. |
| /sessions | List the 20 most recently saved sessions. |
| /resume <id> | Load a saved session. |
| /exit, /quit, /q | Leave. |
Saving sessions
A conversation lives in memory until you run /save. Quitting without saving loses it.
/save writes the full message history, token usage, model and working directory to ~/.supaterra/sessions/<id>.json. Ids look like 20261024-153012-k3x9: the local date and time plus four random characters. Saving again overwrites the same file and keeps its original creation time. /clear starts a fresh id.
❯ /save
saved 20261024-153012-k3x9 → ~/.supaterra/sessions/20261024-153012-k3x9.json
❯ /sessions
20261024-153012-k3x9 claude-opus-5-5 parsePrice in src/utils.ts returns NaN for "€1.299,99"…
❯ /resume 20261024-153012-k3x9
resumed 20261024-153012-k3x9 (14 messages): parsePrice in src/utils.ts returns NaN…A session's title is the first 60 characters of its first message. /resume restores the messages, the usage counters and the model. It also moves back to the saved working directory if that folder still exists; otherwise it warns and stays where you are.
Context & compaction
Every request sends the whole conversation, including tool results. Long sessions eventually approach the model's context window. Compaction replaces the history with a summary the model writes for itself: goals, decisions, files touched, what was verified, open problems and the next step.
- Manually:
/compactat any time. The summary is printed so you can check it. - Automatically: when a request no longer fits, SupaTerra compacts once and retries the turn. This covers both the API rejecting an oversized prompt and the model running out of room mid-reply.
- Even when the history itself is too big to summarize: SupaTerra retries with a shortened plain-text transcript (long tool outputs cut, oldest parts dropped), then a shorter one.
If the context is still exceeded after one compaction, the turn stops with Use /clear to start over.
Tool results are capped at 60,000 characters each, and read_file returns at most 2,000 lines per call. Both keep a single call from flooding the context.
Usage & cost
/cost prints the running totals for the session. One-shot runs print the cost line to stderr at the end when stderr is a terminal.
❯ /cost
requests 14
input 38,210
output 4,112
cache read 31,900
cache write 6,310
cost ~$0.2900The cost is an estimate from a price table built into SupaTerra, matched on the model id, with cache writes priced at 1.25× input. It uses the model that is active when you run /cost for the whole session. An unknown model shows unknown pricing. Your Anthropic Console is the source of truth for billing.
SupaTerra keeps the system prompt and tool list byte-identical between requests so prompt caching keeps hitting; that is what the cache read line measures.
Tools overview
The model works through tools. SupaTerra validates every call's input against a schema before running it, and an invalid call is sent back to the model as an error instead of being executed.
| Tool | Permission | Purpose |
|---|---|---|
| read_file | read | Read a text file with line numbers. |
| list_dir | read | List one directory, folders first, with file sizes. |
| glob | read | Find files by pattern, newest first. |
| grep | read | Search file contents with a regular expression. |
| edit_file | write | Replace an exact string in a file. |
| write_file | write | Create or overwrite a file. |
| bash | exec | Run a shell command. |
| web_search | server | Search the web on Anthropic's servers. Off by default. |
Paths are relative to the working directory unless absolute; ~ expands to your home folder. Directory walks skip node_modules, .git, .hg, .svn, dist, build, out, .next, .nuxt, .turbo, .cache, coverage, __pycache__, .venv, venv, target and .idea.
Reading & searching
These four tools never ask for permission. They can read any path you can, including outside the working directory.
read_file
| Parameter | Type | Description |
|---|---|---|
| path | string | File to read. |
| offset | integer ≥ 1 | First line to return (1-based). Optional. |
| limit | integer ≥ 1 | Maximum lines. Default 2,000. |
Returns line-number⇥text rows and, if the file continues, a footer like [1,240 more lines; total 3,240]. A file with a NUL byte in its first 8,000 bytes is reported as binary instead of being dumped.
list_dir
One parameter, path (default: working directory). Returns a header with counts, then folders (with a trailing /) and files with sizes, each sorted by name. Ignored folders are left out.
glob
| Parameter | Type | Description |
|---|---|---|
| pattern | string | Glob such as **/*.ts or src/**/*.test.*. |
| path | string | Directory to search. Default: working directory. |
Returns up to 500 paths, most recently modified first, then a count of the rest. Skips node_modules and .git, does not match dotfiles, and does not follow symlinks.
grep
| Parameter | Type | Description |
|---|---|---|
| pattern | string | JavaScript regular expression. |
| path | string | Directory or single file. Default: working directory. |
| include | string | Only files matching this glob, for example *.py. |
| case_insensitive | boolean | Match regardless of case. |
| max_results | 1 – 2000 | Stop after this many matches. Default 200. |
Output is file:line: text, with lines longer than 300 characters cut. Binary files, files over 2 MB, ignored folders and hidden folders are skipped.
Editing files
edit_file
| Parameter | Type | Description |
|---|---|---|
| path | string | Existing file to edit. |
| old_string | string | Exact text to replace, including whitespace and indentation. |
| new_string | string | Replacement text. |
| replace_all | boolean | Replace every occurrence. Default false. |
The edit is refused, and the file is left untouched, when:
old_stringis empty or identical tonew_string;old_stringis not found. If it would match after normalizing line endings, the error says the file uses CRLF so the model re-reads it instead of guessing;old_stringappears more than once andreplace_allis not set.
On success it reports the number of replacements, for example Edited src/utils.ts: 1 replacement.
write_file
Parameters path and content. Creates missing parent folders, writes the whole file, and reports Created or Overwrote with the line count. The model is told to prefer edit_file for small changes.
Both tools need permission. A path outside the working directory always asks, even with --yolo. See Permissions.
bash
| Parameter | Type | Description |
|---|---|---|
| command | string | The command line to run. |
| timeout_ms | 1000 – 600000 | Timeout. Default 120,000 (2 minutes). |
| description | string | Short label shown to you next to the command. |
Which shell
| Platform | Shell, in order of preference |
|---|---|
| Windows | Git Bash (bash on PATH, excluding the WSL launcher in System32), then pwsh, then Windows PowerShell with -NoProfile -NonInteractive. |
| macOS / Linux | bash, then zsh, then /bin/sh. |
The chosen shell is named in the system prompt so the model writes commands for it.
How commands run
- In the working directory, with no stdin. Anything that waits for input hangs until the timeout, so the model is told to use non-interactive flags.
- With extra environment variables
SUPATERRA=1,NO_COLOR=1andCI=1(unlessCIis already set), so most tools print plain output. - On timeout or Ctrl C the whole process tree is killed:
taskkill /Ton Windows, the process group on macOS and Linux. - After the shell exits, output is collected for at most one more second. A background job that keeps the pipes open cannot hang the agent.
The result contains stdout, then [stderr] output, a [timed out after … ms] marker when relevant, and always [exit code N].
web_search
An Anthropic-hosted tool: the search runs on Anthropic's servers, not on your machine, and results come back inside the model's reply. It is off by default and limited to 8 searches per request.
supaterra -w # this run
supaterra config set webSearch true # alwaysInside a session, /web toggles it. While it runs, the spinner shows searching the web. Web search may be billed separately by Anthropic.
Permissions
Each tool has a level: read, write or exec. The gate decides per call:
- read tools always run.
- A write outside the working directory always needs a human to say yes, even in yolo mode.
- Otherwise the call runs if auto-approve is on (
--yolo,/yolo,autoApprove) or you answered a for that tool earlier in the session. - If nobody can answer (non-interactive run), the call is denied.
- Otherwise you are asked.
A denial is sent to the model as The user declined this tool call. Do not retry it unchanged; adjust the approach or ask. The system prompt tells it to treat that as a decision, not an obstacle.
Permission simulator
try itThe outside-the-folder check applies to file paths given to edit_file and write_file. A shell command can still touch anything your user account can. Use --yolo only in projects you trust, ideally in a container or VM.
Untrusted repositories
A repository you just cloned can contain files that SupaTerra reads automatically. Two of them matter:
.supaterra.json is restricted
A project config may only set model, effort, maxTokens, showThinking, webSearch and maxIterations. Everything else in it is ignored, in particular:
baseURL: otherwise a repo could send your API key to its own server;autoApprove: otherwise a repo could switch off permission prompts and run commands;apiKeyandfallbacks.
An invalid effort value is dropped too.
Instruction files are part of the prompt
SUPATERRA.md, AGENTS.md or CLAUDE.md in the working directory is added to the system prompt verbatim. In a repository you do not trust, read that file first, keep permission prompts on, and look at each command before you approve it.
Keys & local data
| Path | Contains |
|---|---|
| ~/.supaterra/config.json | Your settings and, after login, the API key in plain text. |
| ~/.supaterra/sessions/*.json | Saved conversations, including every tool result (file contents, command output). |
Set SUPATERRA_HOME to move both somewhere else. Nothing else is written outside your project, and nothing is sent anywhere except the Anthropic API (or your baseURL).
- On shared machines,
chmod 600the config file or keep the key in an environment variable. - The system prompt tells the model not to print
.envfiles or credentials, but read tools can open any file you can. Do not rely on the prompt as a security boundary. configand/configalways mask the key as***.
Config file
Settings live in ~/.supaterra/config.json (print the path with supaterra config path). Edit it by hand or with config set, which validates the key and converts true/false and whole numbers.
supaterra config # effective config, key masked
supaterra config get model # one value
supaterra config set effort xhigh
supaterra config set webSearch true
supaterra config pathConfig playground
live outputAll settings
| Key | Default | Description |
|---|---|---|
| model | claude-opus-5-5 | Model id for every request. |
| effort | high | low · medium · high · xhigh · max. Sent only to models that support it. |
| maxTokens | 64000 | Output tokens per request. Raise it if long edits get cut off. |
| maxIterations | 60 | Tool rounds per message before SupaTerra pauses and waits for you. |
| showThinking | false | Show the summarized thinking. |
| autoApprove | false | Run write and exec tools without asking. Not settable from .supaterra.json. |
| fallbacks | true | Let a supported model hand a refused request to a fallback model on the server. |
| webSearch | false | Offer the server-side web_search tool. |
| baseURL | — | Custom API endpoint (proxy or gateway). Not settable from .supaterra.json. |
| apiKey | — | Written by login. Prefer the environment on shared machines. |
Per-project overrides
Put a .supaterra.json in the project root to change defaults for that folder. Only the safe keys are honored (see Untrusted repositories):
{
"model": "claude-sonnet-5-5",
"effort": "medium",
"maxIterations": 30
}Precedence
Each layer overrides the ones below it. Hover a layer to lift it out of the stack.
Where a setting comes from
top wins- 5Flags:
--model,--effort,--yolo,--thinking,--webfor this run. - 4Environment: API key,
SUPATERRA_MODEL,SUPATERRA_EFFORT, base URL. - 3Project:
.supaterra.jsonin the working directory. - 2User:
~/.supaterra/config.json. - 1Defaults:
claude-opus-5-5,high, 64,000 tokens…
Slash commands like /model sit on top of all of these for the rest of the session, and are not saved.
Environment variables
| Variable | Effect |
|---|---|
| SUPATERRA_API_KEY | API key. Highest priority. |
| ANTHROPIC_API_KEY | API key, used when SUPATERRA_API_KEY is not set. |
| SUPATERRA_MODEL | Model id. |
| SUPATERRA_EFFORT | Effort level. Ignored if it is not a valid level. |
| SUPATERRA_BASE_URL | API endpoint. ANTHROPIC_BASE_URL is used as a fallback. |
| SUPATERRA_HOME | Folder for config.json and sessions/. Default ~/.supaterra. |
| SUPATERRA_DEBUG | Print stack traces for unexpected errors. |
| NO_COLOR | Disable colors. |
| FORCE_COLOR | Force colors even when output is not a terminal. |
A .env.example in the repository shows the common ones. SupaTerra does not load .env files itself; export the variables in your shell (Bun loads .env automatically when running from source).
CLI reference
supaterra [options] # interactive session
supaterra [options] run <prompt...> # one-shot
supaterra [options] <prompt...> # one-shot, run keyword omitted
supaterra login
supaterra config [get <key> | set <key> <value> | path]
supaterra version | help| Flag | Description |
|---|---|
| -m, --model <id> | Model for this run. |
| -e, --effort <lvl> | low · medium · high · xhigh · max. |
| -y, --yolo | Auto-approve write and exec tools. |
| -t, --thinking | Show summarized thinking. |
| -w, --web | Enable web_search. |
| -C, --cwd <dir> | Working directory instead of the current one. |
| -v, --verbose | Print previews of tool output. |
| -V, --version | Print the version. |
| -h, --help | Print help. |
Flags are strict: an unknown flag prints the error and the help text and exits with code 2.
Models, effort & thinking
Any model id the API accepts works with --model. SupaTerra turns on extra features only for models known to support them:
| Feature | What SupaTerra sends | Models |
|---|---|---|
| Effort | output_config.effort | Claude 5 family (Opus, Sonnet, Haiku, Fable), Opus 4.5–4.8, Sonnet 4.6 |
| Adaptive thinking | thinking: adaptive, summarized when showThinking is on | Claude 5 family, Opus 4.6–4.8, Sonnet 4.6 |
| Refusal fallback | server-side fallbacks: "default" (beta) | Opus 5.x, Fable 5.x, Sonnet 5.5 |
| Prompt caching | cache_control on the system prompt and conversation | All |
Choosing an effort
Effort trades thoroughness for speed and tokens. low and medium suit quick questions and small edits; high (default) is a good general setting; xhigh and max are for hard debugging and large refactors where you would rather wait than retry.
When the model declines
If a request is refused, the turn stops with The model declined to continue and the reason the API gives. With fallbacks on and a supported model, the API may instead let a fallback model continue; you then see … declined this request; … continued.
Project instructions
Put house rules for the agent in a markdown file in the project root. SupaTerra uses the first one it finds, in this order: SUPATERRA.md, AGENTS.md, CLAUDE.md. Up to 40,000 characters are appended to the system prompt under Project instructions.
# Project instructions
- Package manager is pnpm. Run tests with `pnpm test`, types with `pnpm typecheck`.
- Never edit files under src/generated/; run `pnpm codegen` instead.
- API handlers live in src/routes/, one file per resource.
- Before saying a change works, run the tests for the files you touched.Good instructions are short and concrete: commands to run, folders to avoid, conventions that are not obvious from the code. The file is re-read when you change folders with /cwd, and read fresh at the start of every run.
The agent loop
One message from you can take many requests to the API. SupaTerra repeats the loop below until the model stops asking for tools.
One user turn
src/agent.tsHow a turn ends
| Stop reason | What SupaTerra does |
|---|---|
| end_turn | The answer is complete. The turn ends. |
| tool_use | Validates and runs each tool call, sends the results, loops. |
| pause_turn | A long server-side step (for example web search) paused; SupaTerra continues automatically. |
| max_tokens | The reply hit maxTokens. If it was in the middle of a tool call, that call is not run (a truncated input could look valid); raise maxTokens or split the task. |
| refusal | Stops and shows the reason. |
| context window exceeded | Compacts once and retries. See Context & compaction. |
Two safety nets: after maxIterations tool rounds (default 60) SupaTerra stops with Send another message to continue., and if a tool call's streamed input is not valid JSON the request is re-sent, at most twice.
The system prompt
It names the tools, the working directory, OS, architecture and shell, and sets the working rules: look before changing, prefer edit_file for targeted edits, run the project's own checks and report real results, respect denials, avoid destructive commands unless asked, keep secrets out of output, lead answers with the result, cite code as path:line, and reply in your language. It contains no timestamps or random ids so it stays identical between requests and keeps the prompt cache warm.
Building binaries
scripts/build.ts cross-compiles single-file executables with bun build --compile --minify. It works from any OS; Bun downloads the target runtime on first use.
bun run build # all five targets into dist/
bun run build:win # windows-x64
bun run build:mac # darwin-arm64, darwin-x64
bun run build:linux # linux-x64, linux-arm64
bun run scripts/build.ts --only linux-arm64 --outdir outThe Windows build gets a title, publisher, version and description, plus an icon if assets/icon.ico exists. The script prints a size and timing table and exits with 1 if any target failed.
CI and releases
| Workflow | Trigger | Does |
|---|---|---|
| ci.yml | push to main, pull requests | Type check, tests, smoke-build linux-x64. |
| release.yml | tag v* | Builds all targets, zips the exe, tars the rest, writes SHA256SUMS.txt, publishes a GitHub Release. |
| pages.yml | changes in web/ | Deploys this website to GitHub Pages. |
bun run typecheck && bun test
git tag v0.1.1 && git push origin v0.1.1 # starts the release workflowAdding a tool
Tools live in src/tools/, one file each, defined with defineTool and a zod schema. The schema is converted to JSON Schema for the API and also validates every call before it runs.
import { z } from "zod";
import { defineTool } from "./types.ts";
import { displayPath, resolvePath } from "./paths.ts";
export const wordCountTool = defineTool({
name: "word_count",
description: "Count the words in a text file.",
permission: "read", // read | write | exec
schema: z.object({
path: z.string().describe("File to count"),
}),
summarize: (i) => `count words in ${i.path}`, // the one-line UI summary
async run(input, ctx) {
const abs = resolvePath(ctx.cwd, input.path);
const file = Bun.file(abs);
if (!(await file.exists())) return `Error: file not found: ${displayPath(ctx.cwd, abs)}`;
const words = (await file.text()).split(/\s+/).filter(Boolean).length;
return `${words} words in ${displayPath(ctx.cwd, abs)}`;
},
});- Register it
Add it to
ALL_TOOLSinsrc/tools/index.ts. Tools are sent sorted by name, so the order there does not affect caching. - Mention it in the system prompt
src/system-prompt.tslists the tool names in its first paragraph; add yours. - Return errors as text
Start the result with
Error:to mark it as failed. Thrown exceptions are caught and reported the same way. - Test
Run
bun run typecheckandbun test.
Keep the CLI dependency-light: the only runtime dependencies are @anthropic-ai/sdk and zod. Imports use explicit .ts extensions.
Troubleshooting
“No API key” although I set one
Environment variables only reach programs started from that shell after they were exported. Open a new terminal, or check with supaterra config get apiKey (prints "***" when a key is found). The lookup order is SUPATERRA_API_KEY → ANTHROPIC_API_KEY → config file.
Edits and commands are always denied in CI
Non-interactive runs cannot show a permission prompt, so write and exec tools are denied. Pass --yolo for that run. Writes outside the working directory stay denied by design.
A command hangs until it times out
Commands get no stdin, so anything that prompts (a password, “Proceed? [y/N]”, an editor) waits forever. Ask the agent to use non-interactive flags such as --yes, -y or --no-edit, or to pass a longer timeout_ms for slow builds (up to 10 minutes).
edit_file keeps failing with “old_string not found”
The text must match byte for byte. The most common cause is CRLF line endings on Windows; the error says so explicitly and the model should re-read the file. If it keeps failing, ask it to re-read the file before editing, or to use write_file for a full rewrite.
The bash tool uses PowerShell on Windows
Git Bash was not found on PATH. Install Git for Windows and make sure bash resolves to it (not to C:\Windows\System32\bash.exe, the WSL launcher).
macOS says the app “cannot be opened”
Gatekeeper quarantines downloaded binaries. Remove the flag with xattr -d com.apple.quarantine /usr/local/bin/supaterra.
“Stopped after 60 tool iterations”
A safety limit per message. Send another message (for example “continue”) and the agent picks up where it stopped. Change the limit with supaterra config set maxIterations 120.
Colors or the spinner look garbled
Your terminal may not support ANSI escapes. Run with NO_COLOR=1. The spinner is only drawn when stderr is a terminal.
Error messages
| Message starts with | Meaning and fix |
|---|---|
| Authentication failed | The API key is invalid or revoked. Run supaterra login. |
| Permission denied by the API | The key's organization cannot use this model or feature. |
| Not found (is the model id right?) | Usually a typo in --model or model. |
| Rate limited | Too many requests or tokens per minute. Wait and retry; SupaTerra already retries up to three times. |
| Bad request | The API rejected the request; the rest of the message says why. |
| API server error | A temporary problem on Anthropic's side. |
| Could not reach the API | Network, proxy or baseURL problem. |
| Error: | Anything else. Set SUPATERRA_DEBUG=1 to print the stack trace. |
Still stuck? Open an issue on GitHub with the command you ran, the output, your OS and supaterra --version. Never paste your API key.