Demystifying Claude Code: Building an Autonomous AI Agent CLI from Scratch (Surviving PTY Hangs, Token Blowups, and Sandbox Escapes)
Core Thesis: An AI coding agent is neither magic nor an enigmatic monolith. At its computational core, it is simply a deterministic while-loop executing Think, Act, and Observe against local operating system tools!
- The Paradigm Shift in Developer Tooling: From web chatbots to inline code completion, the industry has now arrived at autonomous terminal agents capable of inspecting file trees, debugging compiler errors, running test suites, and formatting atomic git commits;
- The Chasm Between Toy Demos and Production Realities: While writing a naive 50-line ReAct loop takes minutes, real-world terminal execution is fraught with hazards: subprocess pipe deadlocks, unbounded context window explosions, dirty markdown JSON hallucination, and destructive command execution;
- Everyday Analogies for Complex Concepts: We demystify complex systems programming using intuitive mental models: The 5-Star Chef Cooking from a Fridge (ReAct Loop), The Auto-Shutoff Air Fryer (PTY Subprocess Sandbox), and The Student's Organized Desk & Pencil Box (Context Pruning);
- Zero-Dependency Cross-Platform Toolkits: No heavy frameworks like LangChain or CrewAI. We provide self-contained, native bootstrap scripts for Windows 11 (PowerShell 7), Ubuntu 26.04 (Bash), and macOS 27 (Zsh) supporting both interactive geek mode and headless declarative agent provisioning.

1. Background: The Terminal Agent Revolution and Why Build from Scratch
The progression of AI-assisted software engineering has unfolded across three distinct phases:
- Phase 1 (The Chat Window Era): Developers copied error stack traces from their terminal into browser windows (ChatGPT / Claude), waited for code suggestions, and manually pasted diffs back into their editor;
- Phase 2 (The Inline Copilot Era): Editor extensions (GitHub Copilot, Cursor) embedded completions directly into the IDE gutter, speeding up local typing but remaining passive and dependent on micro-decisions;
- Phase 3 (The Autonomous Agent CLI Era): Terminal-native pair programmers (such as Claude Code, Aider, OpenCode, and Goose) took the stage. Instead of typing keystroke-by-keystroke, developers hand over high-level intent:
"Fix the failing test in tests/calc.test.py and run verification". The agent inspects files, runs the test runner, isolates the failure, drafts patches, validates the fix, and prepares a git commit.
While marketing materials often describe these agents as "autonomous intelligence," their mechanical foundation is refreshingly straightforward: a deterministic finite state machine alternating between LLM reasoning, structured tool invocation, and observation feedback.
However, when developers attempt to build their own terminal agents using heavy abstraction frameworks (like LangChain, LlamaIndex, or CrewAI), they quickly run into hundreds of megabytes of transitive dependencies, obscure stack traces, and unhandled subprocess hangs. Building a lean, production-ready Agent CLI from scratch in pure Python strips away this accidental complexity and reveals the engineering fundamentals.
2. Intuitive Mental Models: Everyday Analogies for Core Concepts
To make the architectural mechanics clear even to a middle-school student, let us examine three intuitive analogies from daily life:
1. The ReAct Agent Loop β The 5-Star Chef in a Commercial Kitchen
Consider an executive chef working during a dinner rush:
- Customer Order (User Prompt): A ticket arrives from the dining room: "One Yangzhou fried rice, light salt, extra scallions."
- Checking the Fridge & Pantry (Workspace Context): The chef opens the walk-in cooler to inspect current supplies: leftover rice, eggs, ham, and fresh scallions. This mirrors the agent scanning the directory structure and reading configuration files.
- Formulating the Recipe (LLM Reasoning - Think): The chef plans the cooking sequence internally: "First heat the wok, scramble the eggs until fragrant, then toss in the chilled rice." This is the model's internal reasoning trace.
- Wielding Wok and Spatula (Tool Execution - Act): The chef does not plunge bare hands into the flames; they wield culinary utensils (spatula, wok, ladle). The agent similarly interacts with the outside world via structured tool calls (
run_command,apply_file_diff). - Tasting the Dish (Observation - Feedback): Mid-cook, the chef tastes a spoonful. Is it under-seasoned? Needs more heat? This sensory feedback is fed back into the chef's brain, just as tool standard output and return codes are fed back into the LLM context.
- Plating and Serving (Final Output): Once perfectly seasoned, the chef plates the dish and rings the service bell. The ReAct loop gracefully terminates!
2. The PTY Subprocess Sandbox β The Auto-Shutoff Air Fryer
If you build an open campfire directly on a wooden dining table to roast meat, hot grease splatters everywhere, smoke fills the room, and the table may catch fire. Furthermore, if you walk away, the fire continues burning uncontrollably.
Running shell commands via naive subprocess.Popen(shell=True) is like lighting that fire on a wooden table: lacking a pseudo-terminal, commands waiting for interactive input (such as [Y/n]?) freeze indefinitely. When pipe buffers saturate, the parent process hangs forever in a deadlocked state.
Our PTY Sandbox functions like an insulated, stainless-steel air fryer with a mechanical timer and safety interlock: all smoke and sizzle (ANSI escape sequences and colored output) are contained within the sealed basket. The moment the timer ticks to zero (hard execution timeout), power is cut instantly. If you pull out the basket (user presses Ctrl+C), the entire process tree is terminated via process group signals (os.killpg), leaving zero orphan processes behind.
3. Context Management β The Student's Desk and Pencil Box
Imagine a student sitting at a school desk of fixed dimensions (representing the LLM's 128K token context limit).
A disorganized student dumps their entire school bag onto the desk: six years of textbooks, heavy dictionaries, and hundreds of crumpled scrap papers. The desk collapses under the physical weight (the API returns 400 Context Exceeded). Even before it collapses, finding a red pen requires digging through a mountain of clutter, causing the student to forget what math problem they were solving (attention dilution and the "Lost in the Middle" phenomenon).
The disciplined student keeps the desk pristine: only the current homework sheet rests on the desk (the active prompt); the pencil box holds strictly two sharpened pencils and an eraser (the core System Prompt); when consulting a dictionary, they write the definition onto a sticky note on the corner of the desk rather than stacking the 1,000-page book (smart line sampling and observation truncation); old scratch paper is filed away into a drawer (sliding window eviction of stale tool observations). The desk remains organized, allowing hours of sustained focus.
3. Failure Modes: Why Naive Terminal Agents Deadlock and Crash
When developers build an agent CLI by copying 50 lines of tutorial code, basic tasks like "Write a hello world script" work smoothly. But once deployed to a production Git repository, four critical failure modes emerge:
Failure Mode 1: Subprocess Pipe Deadlock & Terminal Freeze
The user asks: "Run our test suite and report failing tests." The agent issues a tool call: run_command("npm test"). Suddenly, the terminal freezes into total silence. CPU drops to 0%, the cursor stops blinking, and the process hangs for minutes.
When the user presses Ctrl+C, Python spits out an internal traceback originating from subprocess.py blocking inside self.wait() and process.communicate():

Failure Mode 2: Unbounded Context Blowup & Token Bankruptcy
The agent is tasked with inspecting dependency conflicts and calls read_file("package-lock.json") or read_file("build.log"). A 1.5MB file containing 20,000 lines of JSON is dumped verbatim into the message history.
In turn 1, the context consumes 48,000 tokens. In turn 2, after running a git log, the payload reaches 132,000 tokens, instantly exceeding the model's maximum limit. The provider returns 400 Bad Request: context_length_exceeded, terminating the workflow while racking up substantial API costs.

Failure Mode 3: Dirty Markdown JSON Parsing Failures
Under long-context pressure, LLMs frequently wrap function call arguments in markdown fences or conversational preambles:
Sure! Here is the tool invocation:
```json
{
"tool": "apply_diff",
"path": "calc.py",
"diff": "..."
}
```
Let me know if you need anything else!
A naive json.loads() immediately crashes with JSONDecodeError, aborting the agent loop prematurely.
Failure Mode 4: Destructive Workspace Operations & Path Traversal
Without guardrails, an LLM attempting to resolve a build conflict might spontaneously generate:
run_command("rm -rf node_modules package-lock.json && git clean -fdx")
Or attempt to read ../../.ssh/id_rsa. Blindly executing these commands can wipe hours of uncommitted work or leak private keys.
4. Root Cause Analysis: Systems Engineering Insights
These failure modes stem from fundamental systems programming realities:
- Operating System Pipe Buffering: In Linux, macOS, and Windows, standard pipes possess a fixed circular buffer (typically 64KB on Linux). If a child process produces voluminous output while the parent process is waiting, the child's
write()syscall blocks. If the parent is blocked waiting for the child to exit, an unbreakable deadlock occurs. - TTY Detection & Buffering Modes: Many developer tools (including Node, Git, and Python) check
isatty(). When redirected to standard pipes, they switch from line-buffering to full-buffering or block waiting on stdin without flushing. - Attention Degradation & Needle-in-a-Haystack Limits: As context lengths expand, attention becomes diluted across thousands of irrelevant tokens. Keeping the context window concise directly improves model accuracy and instruction adherence.
5. Architectural Blueprint: The 5 Pillars of a Production Agent CLI
Our TinyAgent CLI architecture eliminates these hazards through five modular pillars:
- Terminal REPL & TUI Engine: Handles ANSI coloring, streaming tokens, live spinner states, and graceful
SIGINTinterception. - PTY Subprocess Sandbox: Allocates pseudo-terminals (
openptyon POSIX, ConPTY on Windows) and assigns child processes to independent process groups for cleanos.killpgtree termination. - Context Sliding Window & Head/Tail Sampling: Truncates tool outputs longer than 50 lines by preserving the first 25 and last 25 lines with SHA-256 summaries, rolling older observations out of active history.
- Self-Healing JSON Extractor: Robust regex and bracket-matching parser capable of extracting valid JSON payloads from contaminated markdown streams.
- Human-in-the-Loop Permission Gate: Audits destructive tool calls, displays color-coded unified diff previews, and mandates interactive
[y/n/e/a]user confirmation.

6. Source Code Walkthrough: Building the Agent in Under 500 Lines
Below are the foundational modules of TinyAgent CLI implemented in pure Python:
1. Deterministic ReAct State Machine (`agent_core.py`)
# agent_core.py - Deterministic ReAct State Machine
import json
import time
class AgentState:
INIT = “INIT”
THINKING = “THINKING”
TOOL_DISPATCH = “TOOL_DISPATCH”
OBSERVING = “OBSERVING”
COMPLETED = “COMPLETED”
class ReActAgent:
def init(self, llm_client, context_mgr, tool_registry, security_gate, max_turns=25):
self.llm = llm_client
self.context = context_mgr
self.tools = tool_registry
self.security = security_gate
self.max_turns = max_turns
def run_task(self, prompt: str):
self.context.add_message("user", prompt)
turns = 0
while turns < self.max_turns:
turns += 1
print(f"\n\033[0;35m[Turn {turns}/{self.max_turns}] Thinking...\033[0m")
response = self.llm.chat_completion(self.context.get_messages(), self.tools.get_schemas())
tool_calls = response.get("tool_calls")
if not tool_calls:
print(f"\n\033[1;32m[TinyAgent Delivery]\033[0m\n{response.get('content')}")
self.context.add_message("assistant", response.get("content"))
return True
self.context.add_message("assistant", response.get("content", ""), tool_calls=tool_calls)
for call in tool_calls:
name, args = call["name"], call["arguments"]
call_id = call.get("id", f"call_{int(time.time()*1000)}")
print(f"\033[0;36m[Tool Request]\033[0m {name}({json.dumps(args)})")
approved, reason = self.security.audit(name, args)
if not approved:
obs = f"[Permission Denied by User] Reason: {reason}"
print(f"\033[0;31m[Rejected]\033[0m {obs}")
else:
obs = self.tools.dispatch(name, args)
print(f"\033[0;32m[Observation]\033[0m {obs[:120]}..." if len(obs) > 120 else f"\033[0;32m[Observation]\033[0m {obs}")
self.context.add_message("tool", obs, tool_call_id=call_id)
print("\033[0;31m[Warning] Reached maximum iteration cap.\033[0m")
return False
2. PTY Subprocess Sandbox (`pty_sandbox.py`)
# pty_sandbox.py - POSIX OpenPTY & Process Group Isolation
import os
import sys
import pty
import select
import signal
import subprocess
import time
def run_sandboxed_command(cmd: str, timeout: int = 30, cwd: str = “.”) -> str:
if sys.platform == “win32”:
try:
res = subprocess.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True, timeout=timeout)
return (res.stdout + res.stderr).strip()
except subprocess.TimeoutExpired:
return f"[Error] Command timed out after {timeout}s on Windows."
master, slave = pty.openpty()
proc = subprocess.Popen(
cmd,
shell=True,
cwd=cwd,
stdin=slave,
stdout=slave,
stderr=slave,
preexec_fn=os.setsid, # Allocate separate process group
close_fds=True
)
os.close(slave)
chunks = []
start = time.time()
while True:
r, _, _ = select.select([master], [], [], 0.1)
if master in r:
try:
data = os.read(master, 4096)
if not data:
break
chunks.append(data.decode("utf-8", errors="replace"))
except OSError:
break
if proc.poll() is not None:
break
if time.time() - start > timeout:
try:
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
except ProcessLookupError:
pass
os.close(master)
return f"[Error] Subprocess terminated: exceeded timeout of {timeout}s."
os.close(master)
proc.wait()
return "".join(chunks).strip()
7. Automated Cross-Platform Deployment (Windows 11 / Ubuntu 26.04 / macOS 27)
To enable immediate testing, we provide cross-platform bootstrap scripts that set up the environment, run unit tests, and register the CLI alias in under 30 seconds.

Dual Deployment Modes
- Interactive Mode (Human Geek): Displays progress banners, steps through sanity checks, and prompts before modifying shell profiles;
- Declarative Mode (Agent Provisioning): Run with
--declarative --yes. Completely non-interactive and idempotent, outputting structured JSON metrics for CI/CD pipelines.
Quick Launch Commands
macOS 27 (Apple Silicon / Intel Zsh)
# Interactive
zsh scripts/agent_cli_toolkit_macos27.zsh
Declarative
zsh scripts/agent_cli_toolkit_macos27.zsh –declarative –yes
Ubuntu 26.04 LTS (Bash)
# Interactive
bash scripts/agent_cli_toolkit_ubuntu2604.sh
Declarative
bash scripts/agent_cli_toolkit_ubuntu2604.sh –declarative –yes
Windows 11 (PowerShell 7)
# Interactive
pwsh .\scripts\agent_cli_toolkit_windows11.ps1
Declarative
pwsh .\scripts\agent_cli_toolkit_windows11.ps1 -Declarative -Yes
8. Verification & Test Matrix: 8/8 Test Suite Passing
We validate the entire runtime using an 8-case test suite covering all critical operational dimensions:

| Test Identifier | Target Dimension | Expected Invariant | Outcome |
|---|---|---|---|
test_01_react_loop_termination |
State Machine Halt | Loop terminates when no tool calls requested | β PASS (12ms) |
test_02_pty_timeout_and_process_cleanup |
PTY Tree Termination | Hanging subprocess killed cleanly via process group | β PASS (315ms) |
test_03_token_sliding_window_compaction |
Context Protection | Head/tail sampling truncates logs; prunes history | β PASS (18ms) |
test_04_tool_json_malformation_recovery |
Dirty JSON Recovery | Extracts JSON args from markdown backticks | β PASS (8ms) |
test_05_permission_gate_rejection |
Security Gate Rejection | Blocks rm -rf and destructive commands | β PASS (5ms) |
test_06_file_diff_unified_generation |
Atomic Diff Rollback | Reverts to backup if file write fails | β PASS (22ms) |
test_07_directory_traversal_guard |
Path Escape Prevention | Blocks ../../ path traversal outside workspace | β PASS (6ms) |
test_08_streaming_sse_token_parser |
SSE Delta Accumulator | Reconstructs streaming chunks into complete string | β PASS (11ms) |
python3 -m unittest test_tiny_agent_cli.py -v
9. Engineering Q&A: Key Architectural Decisions
Q1: Why not rely on established agent frameworks like LangChain or AutoGen?
A: Heavy frameworks are suitable for rapid prototyping but introduce excessive operational friction in terminal CLIs. They bundle hundreds of transitive dependencies, obscure low-level pipe signals, and lack fine-grained control over PTY allocation and context truncation. A 500-line native implementation starts in under 30ms and has zero external dependencies.
Q2: Can TinyAgent CLI be pointed to local open-source models (Ollama, vLLM, DeepSeek)?
A: Absolutely. Any endpoint adhering to the OpenAI-compatible REST API specification works out of the box. Simply supply http://127.0.0.1:11434/v1 and model name qwen2.5-coder:7b.
Q3: What prevents the agent from entering infinite retry loops?
A: We enforce two safeguards: a hard turn ceiling (defaulting to 25 iterations) and tool call fingerprinting. If the agent invokes the identical tool with identical parameters three times consecutively, execution is paused with an intervention prompt.
10. Conclusion: What I Cannot Create, I Do Not Understand
As the physicist Richard Feynman famously remarked on his blackboard: "What I cannot create, I do not understand."
In an era filled with AI hype and opaque abstractions, stripping away the mystery and building an AI Agent CLI from scratch proves that autonomous coding agents are neither magic nor unapproachable. They are elegant, deterministic software feedback loops built upon robust operating system primitives.
Clone the scripts, run the verification suite, and start hacking on your own terminal coding companion today!