A coding agent CLI built from scratch in a single TypeScript file. It can read, edit, and create files, run shell commands, search the web, and plan before executing. Built with Bun and the Anthropic SDK.
This started as a learning project to understand how tools like Claude Code, Cursor, and Aider work under the hood. Turns out the core is simpler than you'd think.
The whole thing is a while(true) loop:
- Send the conversation to Claude
- Stream the response to the terminal
- If the response has tool calls, execute them
- Send the results back to Claude
- Repeat until Claude responds with just text (no tool calls)
That's the entire architecture. Everything else (permissions, streaming, subagents, persistence) is built on top of this loop.
read_file- read files with line numbersedit_file- find-and-replace with quote normalizationwrite_file- create or overwrite filesglob- list files by patterngrep- search file contents with RE2 (no ReDoS)shell- run shell commandsweb_search- search the web via Anthropic's server toolweb_fetch- fetch a URL, convert to markdown, extract info with Haikuspawn_agent- run a subagent with its own conversation and tools
- Each tool declares its own permission policy:
allow,ask, ordeny - Write tools ask for permission. Read tools auto-allow.
- "Always allow" saves your choice for the rest of the session
- The model never sees the permission prompt. It just gets the result or an error.
- Shell commands have a regex allowlist for safe commands like
ls,pwd,git status
- Toggle with
/plan - Same agent loop, but write tools are blocked and the system prompt tells Claude to plan instead of execute
- When the plan is ready, Claude calls
exit_plan_modewhich shows the plan and asks if you want to proceed
spawn_agentcalls the sameagentLoopfunction recursively- Different system prompt, different tool set, fresh conversation
- The parent only sees the final summary, not the subagent's internal tool calls
- Shares the session (permissions, file timestamps, spinner) with the parent
- Tools marked
isConcurrencySaferun in parallel viaPromise.all - Unsafe tools (edit, write, shell) run one at a time
- Permission prompts always run sequentially (can't show two prompts at once)
- Results are merged back in the original order using a Map lookup
- Responses stream token by token to the terminal
- Text deltas print immediately via
process.stdout.write - Tool use input (JSON) accumulates silently until the block is complete
- Usage stats (input/output/cached tokens) are captured from stream events
- Old tool results are replaced with "[cleared to save context]"
- When token estimate exceeds the threshold, the conversation is summarized by Haiku
- If summarization fails, old messages are dropped as fallback
- Prompt caching on system prompt and tool definitions (90% cheaper on cache hits)
- Each session is stored as a JSONL file (one JSON per line, append-only)
- Sessions live under
~/.tiny-agent/projects/<sanitized-cwd>/ /resumelists past sessions with the first user message as a label/clearstarts a fresh session file
- SDK errors (401, 403, 429, 529, context length, network) are classified and shown as friendly messages
- Tool errors are returned as strings to the model (not thrown)
- Persistence failures warn but don't crash the conversation
- Compaction failures fall back to dropping old messages
- Stream interruptions return partial content instead of losing everything
- Retry with exponential backoff on 429/529 (1s, 2s, 4s)
- All file tools validate paths are inside the working directory (no path traversal)
- Shell command allowlist rejects commands with metacharacters (
;,|,&&, etc.) - Web fetch blocks private IPs, localhost, and AWS metadata endpoint
- Web fetch rejects URLs with embedded credentials
- Session and config files are written with mode 0o600
# install dependencies
bun install
# run directly
bun tiny-agent.ts
# or link globally
bun link
# then run from any project directory
tiny-agentOn first run, you'll be prompted for your Anthropic API key. It gets validated against the API and saved to ~/.tiny-agent/config.json.
You can also set ANTHROPIC_API_KEY as an environment variable (takes precedence over the config file).
| Command | What it does |
|---|---|
/plan |
Toggle plan mode (plan before executing) |
/clear |
Clear conversation and start a new session |
/resume |
Resume a previous session from this directory |
/login |
Update your API key |
/logout |
Clear your API key and exit |
/usage |
Toggle token usage display |
/tokens |
Show estimated token count for current conversation |
It's one file: tiny-agent.ts. That's the whole project.
The file is organized top to bottom:
- Constants and prompts
- Type definitions
- Helper functions (config, path safety, persistence, retry, context management)
- Tool definitions
- Stream handler
- Tool executor
- Agent loop
- Onboarding
- Main entry point
@anthropic-ai/sdk- Claude API client@inquirer/prompts- terminal input and select promptsfast-glob- file pattern matching (used by glob and grep tools)lru-cache- web fetch result cachingora- terminal spinnerpicocolors- terminal colorsre2-wasm- safe regex (no ReDoS, runs as WASM, no native bindings)turndown- HTML to markdown conversion (used by web fetch)zod- schema validation for tool inputs
This project implements the same core patterns:
- Same
while(true)agent loop - Same tool use protocol (
tool_use blocks,tool_resultresponses) - Same permission model (harness-level, hidden from the model)
- Same streaming approach (
content_block_start/delta/stopevents) - Same JSONL persistence format
- Same prompt caching strategy
- Same subagent architecture (recursive loop with isolated conversation)
- Same plan mode design (permission flag + system prompt injection)
- Same quote normalization for edit operations
- Ink (React for terminal) instead of raw stdout
- Ripgrep instead of
fast-glob+fs.readFile - Mid-stream tool execution
- Escape to interrupt (requires Ink for proper
stdinhandling) - Git worktrees for isolated execution
- MCP server integration
- Memory system
- Hooks (user-configured shell commands on events)
- Multi-file tool definitions instead of one big file
This uses the Anthropic API which costs money per token. Some rough numbers:
- Simple question: ~$0.01
- Read and summarize a file: ~$0.02-0.05
- Edit a file with read+edit: ~$0.03-0.08
- Web search + fetch: ~$0.05-0.15
- Subagent exploration: ~$0.10-0.30
Prompt caching helps a lot on multi-turn sessions. After the first turn, the system prompt and tool definitions are cached (90% cheaper for subsequent turns).
Use /usage to toggle per-turn token stats and /tokens to check the current conversation size.
- There's a brief "stuck" feeling when the model generates a
tool_useblock with no preceding text (the terminal shows nothing while tool input JSON streams silently) - Subagents sometimes over-explore if the task description isn't specific enough
- The model occasionally uses markdown despite being told not to
MIT
