TL;DR — We reverse-engineered Claude Code’s agent architecture from its TypeScript source to understand how it handles security, complex tasks, and tool permissions. Then we applied those patterns to an open-source Terraria AI bridge that lets players talk to an LLM inside the game. Here’s what we found, what we built, and what we learned about practical agent design.
Why We Cracked Open Claude Code’s Source
Claude Code isn’t just a coding assistant. Under the hood it’s an agent runtime — it spawns sub-agents, manages file permissions, runs bash commands, and decides when to ask the user vs. just doing the thing. We wanted to understand how it works so we could apply the same ideas to a completely different domain: a Terraria game server.
Our project, terra_llm_bridge, connects a Terraria TShock server to an LLM. Players type @ai in chat and get responses — but the LLM can also act: give items, change weather, teleport players, even toggle hardmode. That last one is where we learned our lesson.
The first time a player asked the AI to set the weather to rain, the LLM autonomously decided to call terra_world_hardmode(confirm=True) — toggling irreversible hardmode for the entire server. No player had asked for it. The model just… did it.
We needed a real permission system. So we went looking at how Claude Code does it.
Claude Code’s 7-Layer Permission Architecture
Reading through ~1,500 lines of src/utils/permissions/permissions.ts plus the Agent tool infrastructure (~3,800 lines), a clear architecture emerged. Claude Code doesn’t have one security check — it has seven:
Layer 1a: Deny rules → "Never allow Bash(git push --force)"
Layer 1b: Ask rules → "Always prompt for Bash(curl *)"
Layer 1c: Tool self-check → Each tool's checkPermissions() method
Layer 1d: Tool self-deny → Read tool whitelists specific paths
Layer 1f: Content-specific rules → "Even in bypass mode, ask for npm publish"
Layer 1g: Safety checks → ".git/, .claude/ are ALWAYS bypass-immune"
Layer 2: Mode-based bypass → bypassPermissions / auto / acceptEdits / dontAsk
Layer 3: YOLO classifier → AI reads the transcript, decides if safe
The most interesting layer is the YOLO classifier — a separate small model that reads the full conversation transcript and classifies each tool call as safe or dangerous. It’s a two-stage system: a fast classifier for obvious cases, and a deeper thinking classifier for edge cases.
But the layer that matters most for our use case isn’t the AI classifier. It’s how Claude Code structurally prevents certain tools from being called in the wrong context — through tool allowlists, denylists, and sub-agent specialization.
The Agent Pattern: Not Multi-Agent, but Specialized Workers
Claude Code doesn’t use multi-agent “collaboration” in the negotiation sense. It uses a single coordinator that spawns specialized workers:
Main Agent (Tool Calling, all tools)
│
├─ Simple: "read file X" → Read tool
│
└─ Complex: "audit this branch" → Agent("Explore")
│
├─ Tools: [Read, Grep, Glob] ← whitelist
├─ Disallowed: [Edit, Write] ← denylist
├─ System prompt: "You are a file search specialist"
└─ Returns findings → Main agent acts on them
Each sub-agent type is defined by three things:
- Tool permissions (allowlist + denylist) — what it can touch
- System prompt — specialized instructions for its role
- Model — Explore agents use Haiku ($) for speed; Plan agents use Sonnet for reasoning
The key insight: the main agent doesn’t get more complex. It stays simple but has ONE tool (Agent) that lets it offload complex work. The sub-agent is just another Tool Calling loop with restricted tools and a different prompt.
This architecture is elegant because it composes: each piece is simple, but the combination handles complexity that would overwhelm a single prompt.
How We Applied This to terra_llm_bridge
Our Terraria bridge has a simpler job than Claude Code — 46 tools instead of hundreds, and the “security” problem is “don’t let the AI toggle hardmode when the player asked about weather” rather than “don’t let the AI rm -rf /”. But the patterns transfer directly.
The Problem
Before: our LLM saw all 46 tools at once. When a player asked “give me the strongest armor set,” the LLM would fire wiki_search AND give_item in parallel — researching while also pre-committing to Solar Flare Armor before reading the wiki results. Sometimes it guessed right. Sometimes it gave a summoner player melee gear.
Our Solution: Two-Phase Tool Access
We didn’t add sub-agents — that would be overkill for 46 tools. Instead, we applied the tool restriction pattern at the graph level:
route → llm(research) ⇄ tool → escalate → llm(action) ⇄ authorize ⇄ tool → output
17 read tools 46 full tools keyword gate
wiki, lookup, status give, kick, spawn
The graph has two phases:
Research phase — the LLM gets only 17 read-only tools (wiki_search, item_lookup, player_list, world_info, etc.). It cannot call give_item, kick, spawn, or any destructive tool. It researches first.
Escalate — when the LLM produces text (no more tool calls needed), the graph automatically flips to action mode and injects a hint: “You now have access to ALL tools.”
Action phase — the LLM gets the full 46-tool set and can act on what it found.
This is structurally enforced. Not a prompt suggestion. The LLM physically cannot call give_item during research because the tool isn’t bound.
The Permission Gate
Before the two-phase split, we also added authorize_node — a hard gate between the LLM and ToolNode that checks whether the player’s recent chat messages contain keywords for the tool’s domain:
|
|
If the player says “set weather to rain” and the LLM tries to call world_hardmode, authorize_node checks: do any of the hardmode keywords appear in the player’s recent messages? No? Blocked. The tool call is replaced with a BLOCKED message before ToolNode ever sees it.
This is a coarse filter — it checks what the player mentioned, not what they requested. “上次打肉山的时候” (last time when I fought Wall of Flesh) would pass the keyword check even though the player didn’t ask for hardmode. But coarse is fine here: the goal is blocking catastrophic mismatches (weather → hardmode), not perfect intent understanding.
What We Chose NOT to Build
No YOLO Classifier
Claude Code’s AI classifier reads the full transcript and classifies tool calls as safe/dangerous. We didn’t build this because:
- It adds latency — an extra LLM call before every gated tool execution
- Terraria chat is low-stakes — a false positive (giving the wrong armor) is fixable
- Keyword matching catches the catastrophic cases
No Sub-Agent Spawning
Claude Code spawns sub-agent processes for complex tasks. We didn’t need this because:
- Terraria tool surface is small (46 tools)
- Multi-turn tool calling handles the complexity we actually face
- Spawning sub-processes for a game chat bot is over-engineering
No ReAct Pattern
The classic Thought → Action → Observation loop would add token overhead without changing our core capability. DeepSeek’s thinking tokens already handle the reasoning, and the two-phase tool access enforces “research before action” more reliably than prompt-based ReAct would.
The Architecture in One Diagram
┌──────────────────────────────────────────────────────────┐
│ Terraria Server (TShock + C# plugin, 24 game hooks) │
│ Player types "@ai give me the best armor" │
└──────────────────────┬───────────────────────────────────┘
│ JSON webhook
┌──────────────────────▼───────────────────────────────────┐
│ Python aiohttp listener (:9876) │
└──────────────────────┬───────────────────────────────────┘
│
┌──────────────────────▼───────────────────────────────────┐
│ LangGraph StateGraph │
│ │
│ route → llm(research) ⇄ tool 17 read tools │
│ │ │
│ escalate → llm(action) ⇄ authorize ⇄ tool │
│ 46 full tools keyword gate │
│ │ │
│ output → broadcast to game chat │
│ │
│ Memory: AsyncSqliteSaver per player (thread_id) │
└──────────────────────────────────────────────────────────┘
│
┌─────────────┴──────────────┐
▼ ▼
TShock REST API Terraria Wiki API
(give / kick / spawn) (terraria.wiki.gg)
Source Diving Lessons
Reading Claude Code’s source taught us three things that apply to any agent project:
1. Security is layered, not binary. A single confirm parameter is a soft suggestion to the LLM. Real security needs structural enforcement — the LLM shouldn’t be able to call a tool it isn’t authorized to use, same way a web server shouldn’t let you access endpoints without authentication, no matter how nicely you ask.
2. Tool restrictions are the cheapest and most reliable form of safety. Claude Code’s Explore agent is “read-only” not because of a prompt — because Edit and Write aren’t in its tool list. Our research phase isn’t “research-first” because of a prompt — because give_item literally isn’t bound. You can’t prompt-inject your way past a tool that doesn’t exist.
3. Specialization beats complexity. Claude Code’s sub-agents aren’t smarter than the main agent — they’re more constrained. Fewer tools + focused prompt = more reliable behavior. Our two-phase system does the same: constrain first, expand only when ready.
The Project
terra_llm_bridge is an open-source project connecting Terraria game servers to LLMs. It features:
- 24 game hooks — custom C# TShock plugin captures chat, boss kills, deaths, logins, and 20 more events
- 46 admin tools — give items, manage players, control weather, spawn NPCs, manage regions and permissions
- Two-phase agent — research (17 tools) → action (46 tools)
- Hard permission gate — keyword-based authorize_node blocks unauthorized tool calls
- MCP server — same 46 tools exposed to Claude Code for server administration
- Persistent memory — per-player conversation history via LangGraph’s AsyncSqliteSaver
The project is currently in active testing and not yet published on GitHub. We’re running it on a private Terraria server, iterating on the agent architecture before open-sourcing. If you’re interested in the code or want early access, reach out.
Built with: Python 3.14, LangGraph 1.x, DeepSeek (Anthropic-compatible API), C# .NET 9, TShock v6.1.0, aiohttp, httpx.