v0: Bash is Everything
After building v1, v2, and v3, a question emerged: What is the essence of an Agent?
v0 answers this through reverse thinking—strip everything away until only the core remains.
Core Insight
Unix philosophy: Everything is a file, everything can be piped. Bash is the gateway to this world:
| You need | Bash command |
|---|---|
| Read files | cat, head, grep |
| Write files | echo '...' > file |
| Search | find, grep, rg |
| Execute | python, npm, make |
| Subagent | python v0_bash_agent.py "task" |
The last line is the key insight: calling itself via bash implements subagents. No Task tool needed, no Agent Registry—just recursion.
Complete Code
#!/usr/bin/env python3
"""
v0_bash_agent.py - Mini Claude Code: Bash is All You Need (~50 lines)
Core Philosophy: "Bash is All You Need"
======================================
This is the ULTIMATE simplification of a coding agent. After building v1-v3,
we ask: what is the ESSENCE of an agent?
The answer: ONE tool (bash) + ONE loop = FULL agent capability.
Usage:
# Interactive mode
python v0_bash_agent.py
# Subagent mode (called by parent agent or directly)
python v0_bash_agent.py "explore src/ and summarize"
"""
from anthropic import Anthropic
from dotenv import load_dotenv
import subprocess
import sys
import os
load_dotenv(override=True)
# Initialize Anthropic client (uses ANTHROPIC_API_KEY and ANTHROPIC_BASE_URL env vars)
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
MODEL = os.getenv("MODEL_ID", "claude-sonnet-4-5-20250929")
# The ONE tool that does everything
# Notice how the description teaches the model common patterns AND how to spawn subagents
TOOL = [{
"name": "bash",
"description": """Execute shell command. Common patterns:
- Read: cat/head/tail, grep/find/rg/ls, wc -l
- Write: echo 'content' > file, sed -i 's/old/new/g' file
- Subagent: python v0_bash_agent.py 'task description' (spawns isolated agent, returns summary)""",
"input_schema": {
"type": "object",
"properties": {"command": {"type": "string"}},
"required": ["command"]
}
}]
# System prompt teaches the model HOW to use bash effectively
# Notice the subagent guidance - this is how we get hierarchical task decomposition
SYSTEM = f"""You are a CLI agent at {os.getcwd()}. Solve problems using bash commands.
Rules:
- Prefer tools over prose. Act first, explain briefly after.
- Read files: cat, grep, find, rg, ls, head, tail
- Write files: echo '...' > file, sed -i, or cat << 'EOF' > file
- Subagent: For complex subtasks, spawn a subagent to keep context clean:
python v0_bash_agent.py "explore src/ and summarize the architecture"
When to use subagent:
- Task requires reading many files (isolate the exploration)
- Task is independent and self-contained
- You want to avoid polluting current conversation with intermediate details
The subagent runs in isolation and returns only its final summary."""
def chat(prompt, history=None):
"""
The complete agent loop in ONE function.
This is the core pattern that ALL coding agents share:
while not done:
response = model(messages, tools)
if no tool calls: return
execute tools, append results
Args:
prompt: User's request
history: Conversation history (mutable, shared across calls in interactive mode)
Returns:
Final text response from the model
"""
if history is None:
history = []
history.append({"role": "user", "content": prompt})
while True:
# 1. Call the model with tools
response = client.messages.create(
model=MODEL,
system=SYSTEM,
messages=history,
tools=TOOL,
max_tokens=8000
)
# 2. Build assistant message content (preserve both text and tool_use blocks)
content = []
for block in response.content:
if hasattr(block, "text"):
content.append({"type": "text", "text": block.text})
elif block.type == "tool_use":
content.append({
"type": "tool_use",
"id": block.id,
"name": block.name,
"input": block.input
})
history.append({"role": "assistant", "content": content})
# 3. If model didn't call tools, we're done
if response.stop_reason != "tool_use":
return "".join(b.text for b in response.content if hasattr(b, "text"))
# 4. Execute each tool call and collect results
results = []
for block in response.content:
if block.type == "tool_use":
cmd = block.input["command"]
print(f"\033[33m$ {cmd}\033[0m") # Yellow color for commands
try:
out = subprocess.run(
cmd,
shell=True,
capture_output=True,
text=True,
timeout=300,
cwd=os.getcwd()
)
output = out.stdout + out.stderr
except subprocess.TimeoutExpired:
output = "(timeout after 300s)"
print(output or "(empty)")
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output[:50000] # Truncate very long outputs
})
# 5. Append results and continue the loop
history.append({"role": "user", "content": results})
if __name__ == "__main__":
if len(sys.argv) > 1:
# Subagent mode: execute task and print result
# This is how parent agents spawn children via bash
print(chat(sys.argv[1]))
else:
# Interactive REPL mode
history = []
while True:
try:
query = input("\033[36m>> \033[0m") # Cyan prompt
except (EOFError, KeyboardInterrupt):
break
if query in ("q", "exit", ""):
break
print(chat(query, history))That’s the entire agent. ~50 lines.
How Subagents Work
Main Agent
└─ bash: python v0_bash_agent.py "analyze architecture"
└─ Subagent (separate process, fresh history)
├─ bash: find . -name "*.py"
├─ bash: cat src/main.py
└─ Returns summary via stdoutProcess isolation = context isolation
- Subprocess has its own
history=[] - Parent process captures stdout as tool result
- Recursive calls enable infinite nesting
What v0 Sacrifices
| Feature | v0 | v3 |
|---|---|---|
| Agent types | None | explore/code/plan |
| Tool filtering | None | Whitelist |
| Progress display | Plain stdout | Inline updates |
| Code complexity | ~50 lines | ~450 lines |
What v0 Proves
Complex capabilities emerge from simple rules:
- One tool suffices — Bash is the gateway to everything
- Recursion = hierarchy — Self-calls implement subagents
- Process = isolation — OS provides context separation
- Prompt = constraints — Instructions shape behavior
The core pattern never changes:
while True:
response = model(messages, tools)
if response.stop_reason != "tool_use":
return response.text
results = execute(response.tool_calls)
messages.append(results)Everything else—todos, subagents, permissions—is refinement around this loop.
Running Examples
# Install dependencies
pip install anthropic python-dotenv
# Configure environment variables (create .env file)
echo "ANTHROPIC_API_KEY=sk-ant-xxx" > .env
# Optional: Configure custom endpoint (uses official API by default)
# Interactive mode
python v0_bash_agent.py
# Subagent mode
python v0_bash_agent.py "Analyze current directory structure and summarize"Common Questions
Q: Why not just use v1 directly? A: v0 is good for building intuition. Understanding that “one tool can work too” makes the tool separation in v1 more appreciable.
Q: Won’t subagents waste tokens? A: No. Subagents run independently and only return summaries to the parent. This actually reduces noise in the main context.
Q: Can I use v0 in production? A: Not recommended. Lacks safety guardrails, permission systems, progress display, etc. v0 is a minimal implementation for teaching.
Bash is everything.