δΈ­ζ–‡ English

Demystifying Claude Code: Building an Autonomous AI Agent CLI from Scratch (Surviving PTY Hangs, Token Blowups, and Sandbox Escapes)

Published: 2026-10-14 Β· Views --
AI Agent CLI LLM Architecture Terminal Automation Troubleshooting Python Source Code Windows 11 Ubuntu 26.04 macOS 27

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.

Technical Architecture Hero Banner: Building an AI Agent CLI from Scratch


1. Background: The Terminal Agent Revolution and Why Build from Scratch

The progression of AI-assisted software engineering has unfolded across three distinct phases:

  1. 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;
  2. 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;
  3. 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:

Everyday Analogy: The 5-Star Chef in a Commercial Kitchen and the ReAct Loop

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.

Everyday Analogy: The Auto-Shutoff Air Fryer and PTY Subprocess Sandbox

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.

Everyday Analogy: The Student’s Desk & Pencil Box and Context Window Management


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():

Real Screenshot: Naive Agent Suffering Standard Pipe Buffer Deadlock

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.

Real Telemetry Comparison: Naive Context Explosion vs Smart Sliding Window

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:

  1. 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.
  2. 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.
  3. 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:

Complete System Architecture: Production AI Agent CLI

  1. Terminal REPL & TUI Engine: Handles ANSI coloring, streaming tokens, live spinner states, and graceful SIGINT interception.
  2. PTY Subprocess Sandbox: Allocates pseudo-terminals (openpty on POSIX, ConPTY on Windows) and assigns child processes to independent process groups for clean os.killpg tree termination.
  3. 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.
  4. Self-Healing JSON Extractor: Robust regex and bracket-matching parser capable of extracting valid JSON payloads from contaminated markdown streams.
  5. 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.

Real Screenshot: Human-in-the-Loop Security Gate Intercepting Dangerous Operation


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.

Real Screenshot: Cross-Platform Bootstrap Matrix Across Win11, Ubuntu 26.04, and macOS 27

Dual Deployment Modes

Declarative Pipeline: AI Agent CLI Declarative Provisioning SOP

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:

Real Screenshot: Automated Test Suite Passing 8/8 Test Cases

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!

Comments

Sign in with GitHub to comment. Chinese and English versions share the same discussion. Discuss on GitHub

Views --