ShanClaw Runtime Repo-Verified + External Context

Kocoro Lab · 2026.04 · REF SC-ARCH-003
PATH 1
ShanClaw Only
Single machine, single user.
TUI / one-shot CLI run AgentLoop directly.
HTTP API is served by the daemon.
No Shannon needed.
PATH 2
Shannon (External) + ShanClaw
External orchestrator sends work to ShanClaw.
Upstream internals are not verified in this repo.
ShanClaw integration is code-backed here.
PATH 3
Cloud + Shannon (External) + ShanClaw
Multi-channel delivery can flow through external adapters.
Cloud/orchestrator internals are outside this repo.
Only ShanClaw's side is verified here.
Via Cloud (external)
Slack
webhook adapter
LINE
webhook adapter
Feishu
webhook adapter
Telegram
webhook adapter
External clients (not verified here)
Desktop
external app
Web UI
external app
API / SDK
external API
Direct to ShanClaw
TUI
Bubbletea terminal
CLI
shan "prompt"
HTTP API
63 routes · POST /message
Custom Bot
Discord etc.
Shannon Cloud (External)
Channel adapters / rendering / approval UX — not verified from this repo
EXTERNAL CONTEXT
CHANNEL ADAPTERS
Slack
external adapter
LINE
external adapter
Feishu
external adapter
Telegram
external adapter
EXTERNAL SERVICES
Formatting Layer
External implementation, not verified here
Approval Surface
External implementation, not verified here
EXTERNAL OPS
Auth / Tenancy
External implementation, not verified here
Operational Policies
External implementation, not verified here
↓ External forwarding path (not verified here)
Shannon (External)
Upstream orchestrator interfacing with ShanClaw — not verified from this repo
EXTERNAL CONTEXT
EXTERNAL API
Ingress / Auth
External implementation, not verified here
Streaming / Callbacks
External implementation, not verified here
Daemon Transport
ShanClaw speaks WS claim / reply / event
Approval Relay
Protocol visible; backend impl not verified
EXTERNAL ORCHESTRATION
Workflow Control
External implementation, not verified here
Routing / Fan-out
External implementation, not verified here
Budget / Policy
External implementation, not verified here
Dispatch Layer
External implementation, not verified here
EXTERNAL CONCERNS
routingexternal
workflowexternal
researchexternal
parallelismexternal
policyexternal
evaluationexternal
EXTERNAL EXECUTION
Execution Runtime
External implementation, not verified here
Policy Enforcement
External implementation, not verified here
Failure Handling
External implementation, not verified here
REMOTE SANDBOX
Isolation Layer
External implementation, not verified here
Workspace Mgmt
External implementation, not verified here
EXTERNAL MODEL ACCESS
Gateway / Provider Router
External implementation, not verified here
Remote Agent Workflows
External implementation, not verified here
Server-side Tools
External implementation, not verified here
Quality / Review
External implementation, not verified here
EXTERNAL TOOLS
remote_search
external
remote_exec
external
remote_fetch
external
other_tools
external
External Infra
Deployment
not verified here
Storage
not verified here
Messaging
not verified here
Search / Memory
not verified here
⇅ WebSocket · claim / reply / event · auto-reconnect
claim / claim_ack · approval_request / approval_response · 15s heartbeat · up to 5 concurrent agents
ShanClaw
Go — local runtime — shan daemon :7533 — open source
LOCAL RUNTIME · OPEN SOURCE
DAEMON CORE
WS Client
Connect to Shannon · Reconnect
HTTP API Server
:7533 · 63 routes · POST /message + SSE
Session Router
SessionCache · Per-route lock
Approval Broker
Interactive WS relay · auto-approve
Output Profiles
markdown (local) · plain (channels)
AGENT LOOP
AgentLoop.Run()
25-75 iterations · SwitchAgent()
Context Window
85% compact · 2-phase summary
Loop Detectors (×9)
Consecutive · Spread · Error · Sleep
Hallucination Guard
Fabricated calls · Unverified claims
Disk Spill
>50K → temp file + 2K preview
Tool Partition
Read-only batch (sem=10) · Write serial
PERMISSIONS
6-Layer Model
hard-block → denied → compound split → allowed → default safe → ask
Read Tracker
Must file_read before file_edit
Audit Logger
JSON-lines · RedactSecrets
SESSION
Session Manager
JSON persistence · SQLite FTS5
Scheduler
launchd plist · cron · atomic writes
Named Agents
Prompt · Memory · attached global skills
LOCAL TOOLS (+ CONDITIONAL)
bash
shell exec
browser
chromedp
file_read
read files
file_edit
edit files
screenshot
macOS screen
accessibility
GUI control
clipboard
copy · paste
cloud_delegate
remote task
applescript
macOS auto
grep · glob
search files
Base local tools: 26 · runtime extras include session_search and cloud_delegate
MCP INTEGRATIONS
GitHub
Slack
Database
Gmail
Custom
stdio · HTTP transports · extensible
Modes
Daemon
WS + HTTP :7533
TUI
Bubbletea interactive
One-shot CLI
shan "prompt"
MCP Server
JSON-RPC stdio
LLM Access
ShanClaw uses a gateway client
or Ollama directly
Gateway Client
Remote models via external service
Ollama
Local models · direct
Specific upstream providers live
behind the gateway, outside this repo
External upstream path (not repo-verified)
Shannon → ShanClaw (repo-visible WebSocket contract)
Direct to ShanClaw (no Shannon needed)

Runtime Internals — A Deeper Look

Kocoro Lab · 2026.04 · REF SC-DEEP-003 · current codebase snapshot

Section 1 above sketched the topology — daemon, transports, agent loop, permissions. This second section traces the actual call path through the codebase and surfaces the runtime details that matter once you start reading internal/agent/loop.go for real: the named loop detectors, three-phase tool execution, compaction sequence, registry sort, EventBus ring buffer, and the rest.

Recent Additions (Verified In Current Code)
Feature highlights present in the current codebase snapshot
NEW
Ollama providerLocal models via OpenAI-compat API
EventBus ring bufferSSE reconnect replay
Session summary APIGET /sessions/{id}/summary
Session renameTUI + PATCH /sessions/{id}
State-aware cachestatecache.go — bind to state
Result shaperesultshape.go — tree reads
Deferred file_refImages/PDF in file_read
PDF reader skillBundled skill
Multimodal contentRequestContentBlock in payload
RunStatus packageRun state tracking
TUI overhaulDoctor · /compact · new commands
Chrome profile controlsDaemon config + profile API

1. Message Flow — From Input to Response

Entry Path: shan "hello" (One-shot CLI)
The simplest possible path — follow this first to understand ShanClaw
TRACED
FileWhat happens
cmd/root.gorootCmd.RunE — Cobra entry point, loads config
cmd/root.goif len(args) > 0 → one-shot mode, else TUI
cmd/root.gorunOneShot(cfg, query, agentOverride) — sets up environment
cmd/root.gotools.RegisterAll(gw, runCfg, agentOverride) — registers local + MCP + gateway tools
cmd/root.goagent.NewAgentLoop(...) — creates the loop object (no AI yet)
cmd/root.goloop.Run(ctx, query, nil, nil)AI actually runs here
cmd/root.gofmt.Print(renderMarkdown(result)) — render markdown, then print usage summary
Key insight: runOneShot() is just "setting the table" — loading config, building the tool registry, creating the audit logger. All the actual AI work happens in one line: loop.Run(ctx, query, nil, nil). The 4 arguments are: cancel signal, user message, multimodal content, history (nil for one-shot).
Complete Request Lifecycle
POST /message → AgentLoop → Reply
DAEMON PATH
1
Receive
HTTP POST /message
or WS message from Cloud
2
Route
ComputeRouteKey
source:channel → session
3
Lock & Resume
Per-route mutex
Resume or create session
4
Setup
Clone registry
Load skills · Set config
5
AgentLoop.Run()
Iterate
LLM + tools + compaction
6
Persist
Save messages
Emit events · Unlock
7
Reply
JSON or SSE
Back to client/Cloud
WebSocket Protocol — Extra Message Types
CLOUD ↔ DAEMON

Beyond the basics already covered in Section 1 (claim/claim_ack, approval_request/response, heartbeat). These are the additional message types observed on the wire:

Cloud → Daemon

TypePurpose
connectedWS session established
messageIncoming user message payload
systemSystem broadcast notification

Daemon → Cloud

TypePurpose
replyAgent final response
eventTool/LLM streaming events
progressHeartbeat + workflow_id
approval_resolvedDaemon resolved approval before channel relay
proactiveUnsolicited agent message
disconnectClose WebSocket session cleanly
Session Routing
ROUTE KEY
ConditionRoute KeyBehavior
agent setagent:nameSingle long-lived session
session_id setsession:idResume exact session
source + channeldefault:src:chConversation continuity
Neither set""Fresh session every time

Bypass Sources (always fresh)

"" (empty) web webhook cron schedule system

Output Format by Source

slack, line, feishu, lark, telegram, webhookplain text
everything else (discord, shanclaw, custom)markdown

2. Agent Loop — The Core Engine

Preparation Phase (before the loop starts)
internal/agent/loop.go — Run() does setup steps before the first LLM call
TRACED
1. Reset State
injectedMessages = nil
runMessages = nil
runMsgInjected = nil
runMsgTimestamps = nil
lastRunStatus = {}
Each Run() is a fresh start — no leftover state from last call
2. WorkingSet Sync
SyncToolset(a.tools)
Cache current tool list for deferred mode schema resolution
3. Deferred Mode
deferredMode ?tools > schema budget
If too many tools, hide cold ones behind tool_search
4. CWD & Instructions
LoadInstructions
WithSessionCWD
Never falls back to os.Getwd() — daemon CWD leaks were a real bug fix
5. Base Prompt
persona+ rules + examples
Named agents override persona; rules always present
6. Memory + Effective Tools
LoadMemoryFrom
effToolsclone only in deferred mode
Never overwrite a.tools — deferred mode clones to add tool_search; normal mode reuses the shared registry directly
Copy-on-Write pattern: a.tools is shared across multiple concurrent sessions in daemon mode. Direct mutation would corrupt other runs. Deferred mode clones the registry into effTools, adds tool_search, and throws away the clone when Run() returns. This is one of several "shared = read-only, modify = use a copy" patterns throughout ShanClaw.
AgentLoop.Run() — Main Iteration
internal/agent/loop.go · main iteration loop
CORE
A
Drain Injected
Mid-run follow-up messages from same route
B
Delta + Checkpoint
Poll TemporalDelta
Inject progress checkpoint near 60%
C
Context Hygiene
Filter old images (keep 5)
Compress old tool results
D
Compaction Check
If over threshold:
PersistLearnings → Summary → Shape
E
LLM Call
CompleteStream via gateway
Retry 3x on transient errors
F
Response Parse
Text only → return
Has tools → batch and execute
G
Result Shape
Post-tool shaping
State-aware cache update
H
Loop Detection
9 detectors check
Nudge or force-stop
↺ repeat until: text-only response, max iterations reached, or force-stop triggered
Streaming Truth — ShanClaw is NOT character-by-character
Despite having SetEnableStreaming, the user-visible effect is "chunks appear at once", not a typewriter effect
SURPRISING
ModeenableStreamingActual UX
TUItrue"streaming enabled but deltas are suppressed — only final text rendered"
Daemon runnerfalseNo streaming at all — wait for full response
One-shot CLI(no handler)No streaming — wait for full response, then print
Why? Streaming character-by-character breaks Markdown rendering (code blocks, tables, lists look broken until complete). ShanClaw chose "correctness over typewriter effect". The enableStreaming=true in TUI is just to enable cancellation and partial-result preservation mid-stream — the UI still waits for complete chunks before rendering.

What you actually see: each tool call result appears as a block, then the final text appears as a block. Chunks, not characters. A short message from the daemon SSE endpoint usually returns with only a done event — no delta events at all, even though the API supports them.

Tool Execution Pipeline — 3 Phases

PHASE 1 — SERIAL
Permission & Approval
  • Deduplicate identical tool calls
  • Check denied-call cache
  • Cross-iteration dedup cache
  • Resolve tool from registry
  • 6-layer permission check
  • RequiresApproval + SafeChecker
  • Approval cache (same call = skip)
  • Pre-tool-use hook
PHASE 2 — BATCHED
Partitioned Execution
  • partitionToolCalls() groups by read-only
  • Consecutive reads → 1 concurrent batch
  • Writes → individual sequential batches
  • Semaphore: max 10 concurrent tools
  • Panic recovery per tool call
  • Update ReadTracker after each batch
  • Deferred: check if tool_search loaded schemas
PHASE 3 — SERIAL
Post-Processing
  • Post-tool-use hook
  • Sanitize results (strip base64, line numbers)
  • Audit logging (JSON-lines, RedactSecrets)
  • Fire OnToolResult events
  • Disk spill: oversized → temp file + 2K preview
  • Truncate to ~30K chars (resultTrunc default)
  • Record in LoopDetector
  • Cache for cross-iteration dedup
Hallucination Detection (3 checks)
Fabricated callsText output looks like tool invocation
Unverified claimsClaims completion without tool evidence
Success after denialClaims denied tool actually ran
Max 2 nudges → continue loop for correction
Loop Detection — 9 Named Detectors (sliding window: 20 calls)
ToolModeSwitch
Visual after GUI success → nudge
SuccessAfterError
Visual after error fix → nudge
ConsecutiveDuplicate
≥2 nudge · ≥3 force-stop
ExactDuplicate
≥3 nudge · ≥6 force-stop
SameToolError
≥4 nudge · ≥8 force-stop
FamilyNoProgress
≥3/5/7 tiered escalation
SearchEscalation
≥5 nudge · ≥8 force-stop
NoProgress
≥8 / ≥16 · bash ≥12 nudge
Sleep
≥2 sleep → nudge · ≥4 stop
Escalation: nudge (try different approach) → 3 nudges → force-stop (final LLM call + exit)

3. Context Management

Proactive Compaction Sequence
Triggertokens over compaction threshold
Step 1PersistLearnings() → MEMORY.md
Step 2GenerateSummary() via small-tier LLM
Step 3ShapeHistory() → keep last 3-20 turns
Two-phase summary: analysis scratchpad → distilled summary
Reactive Compaction
TriggerLLM returns context-length error
SoftCompress results + summary + shape
EmergencyUltra-aggressive (1 char per result)
FlagreactiveCompacted = true (no loops)
Result Compression (by age)
Tier 3 (0-2 msgs)Full content
Tier 2 (3-10 msgs)Head + tail truncation
Tier 1 (>10 msgs)Metadata only
Disk Spilloversized → temp file + 2K preview

System Prompt — 3-Layer Architecture

Static SystemPersona + rules + tool list + skills
+
Stable ContextSticky session facts (source, channel, sender)
+
Volatile ContextMemory (2K) · Instructions (16K) · Date · CWD · MCP
=
Full PromptCached by gateway for efficiency

4. Tool System — Registry Priority & Cache Economics

Tool Registry Priority
Local > MCP > Gateway · Dedup by name
Tool Interface (required)
Info() ToolInfoname, description, param schema
Run(ctx, argsJSON)execute, return ToolResult
RequiresApproval() booldoes this need user confirm?
Optional interfaces (opt-in behavior)
ReadOnlyCheckercan batch concurrently
SafeCheckerskip approval for safe args
SafeCheckerWithContextsession-aware safe args
NativeToolProvideruse provider's native format
ToolSourcerdeclare origin (local/MCP/gw)
Why tool lists are stable-sorted — prompt cache economics
SortedSchemas() groups tools as local (α-sorted) → MCP (α-sorted) → gateway (α-sorted), then flattens. Why so much care about ordering?

Because Anthropic / OpenAI cache the system prompt + tools list. If the order of tools is identical across requests, the provider charges you ~10% of the full input cost for that prefix. If the order shuffles (like Go's random map iteration would produce), every single request is a cache miss. On a long session with dozens of tools and thousands of tokens of tool schemas, this is the difference between fast+cheap and slow+expensive.

A 3-line sort.Strings(local) is one of the highest-leverage micro-optimizations in the entire codebase.

File Operations

file_read (images + PDF) file_write file_edit glob grep directory_list
file_read now supports deferred file_ref for images and PDFs (via bundled pdf-reader skill)

Shell & System

bash process system_info think http clipboard notify memory_append

macOS GUI Automation

accessibility applescript screenshot computer browser wait_for ghostty

Session & Schedule

session_search schedule_create schedule_list schedule_update schedule_remove

Cloud & Skills

cloud_delegate use_skill tool_search (deferred mode)

Gateway Server Tools (remote)

web_search web_fetch web_crawl page_screenshot analytics

5. Configuration — Multi-Level Merge

Global~/.shannon/config.yaml
Project.shannon/config.yaml
Local.shannon/config.local.yaml
Agent Overrideagent/config.yaml
Scalars override · Lists merge + dedup · MCP servers: _inherit flag controls overlay vs replace
Agent Settings
max_iterations25 (default) → 75 (GUI)
temperature0.0
max_tokens32,000
context_window128,000
thinking_modeadaptive
thinking_budget10,000 tokens
Tool Settings
bash_timeoutconfigurable
bash_max_outputconfigurable
result_truncation30K chars (default)
grep_max_resultsconfigurable
Daemon Settings
auto_approvefalse (interactive)
chrome_profileoptional explicit Chrome profile
Port, concurrency, and heartbeat are hardcoded runtime defaults, not daemon config fields

6. Agent Definitions & Storage

Agent File Structure
AGENT.mdSystem prompt (persona + rules)
MEMORY.mdPersistent memory (runtime dir)
config.yamlModel, iterations, MCP, tools filter
commands/*.mdSlash commands (max 8K chars)
_attached.yamlAttached-skill manifest (primary model)
installed skillsresolved from global skills; bundled skills may auto-inject
Legacy agent-local skill directories are cleaned up; the current model is attachment + global install
Location: ~/.shannon/agents/<name>/
Session & Storage
sessions/*.jsonFull message history
sessions/sessions.dbSQLite FTS5 search index
schedules.jsonCron schedules (flock + atomic)
logs/audit.logJSON-lines tool audit trail (RedactSecrets)
tmp/tool_result_*Disk spill files (auto-cleanup)
Base: ~/.shannon/ · Per-agent: ~/.shannon/agents/<name>/sessions/

7. Daemon HTTP API — Surface Map

Current Surface (63 Routes)
internal/daemon/server.go · localhost only · grouped below

Core

POST/messageRun agent (JSON or SSE)
POST/approvalSubmit tool approval
POST/cancelCancel in-flight run
GET/eventsSSE event stream (EventBus replay via SubscribeWithReplay)

Agents

GET/agentsList all agents
GET/agents/{name}Get agent details
POST/agentsCreate agent
PUT/agents/{name}Update agent
DELETE/agents/{name}Delete agent
PUT/agents/{name}/configUpdate config
DELETE/agents/{name}/configDelete config override
PUT/agents/{name}/commands/{cmd}Set command
DELETE/agents/{name}/commands/{cmd}Delete command
PUT/agents/{name}/skills/{skill}Attach skill
DELETE/agents/{name}/skills/{skill}Detach skill

Skills & Marketplace

GET/skills/downloadableList installable bundled skills
GET/skillsList global skills
GET/skills/{name}Get skill details
PUT/skills/{name}Create/update skill
DELETE/skills/{name}Delete skill
GET/skills/marketplaceBrowse marketplace
GET/skills/marketplace/entry/{slug}Marketplace entry detail
POST/skills/install/{name}Install skill
POST/skills/marketplace/install/{slug}Install from marketplace
GET/skills/{name}/usageList agents attaching a skill
GET/skills/{name}/scriptsList skill scripts
PUT/skills/{name}/scripts/{filename}Write skill script
DELETE/skills/{name}/scripts/{filename}Delete skill script
GET/skills/{name}/referencesList skill references
PUT/skills/{name}/references/{filename}Write skill reference
DELETE/skills/{name}/references/{filename}Delete skill reference
GET/skills/{name}/assetsList skill assets
PUT/skills/{name}/assets/{filename}Write skill asset
DELETE/skills/{name}/assets/{filename}Delete skill asset

Sessions

GET/sessionsList sessions
GET/sessions/{id}Load session
DELETE/sessions/{id}Delete session
PATCH/sessions/{id}Patch session title
POST/sessions/{id}/editTruncate + re-run
GET/sessions/{id}/summaryNEW · Auto summary
GET/sessions/searchFTS5 keyword search

Config & Health

GET/configGet config (redacted)
GET/config/statusEffective config status
PATCH/configMerge-patch config
POST/config/reloadReload + restart MCPs
GET/instructionsGet global instructions
PUT/instructionsReplace global instructions
GET/healthHealth + version
GET/statusUptime · Connection

Schedules

GET/schedulesList schedules
GET/schedules/{id}Get schedule
POST/schedulesCreate schedule
PATCH/schedules/{id}Update schedule
DELETE/schedules/{id}Delete schedule

Permissions & Chrome

GET/permissionsmacOS TCC status
POST/permissions/requestRequest desktop permissions
GET/chrome/statusPlaywright CDP status
GET/chrome/profileGet Chrome profile mode
POST/chrome/profileSet Chrome profile mode
POST/chrome/profile/refreshRefresh cloned profile
POST/chrome/showShow managed Chrome window
POST/chrome/hideHide managed Chrome window

System

POST/shutdownGraceful shutdown

8. MCP Client/Server & Playwright CDP

MCP Client Manager
DiscoveryConnectAll() → ListTools → cache
ResilienceAuto-reconnect on transport error
Idle timeoutDisconnect after inactivity (non-keepAlive)
SupervisorHealth monitoring + on-demand restart
PlaywrightDisables legacy browser tools when connected
MCP Server Mode
Methodsinitialize · tools/list · tools/call
Permissions6-layer checks enforced
ApprovalAsk → deny (no TTY in MCP)
HooksPre/post tool-use hooks fire
AuditAll calls logged
Playwright CDP Integration — Special-Cased MCP
internal/mcp/chrome.go — ShanClaw auto-manages a dedicated Chrome instance for Playwright MCP
DEEP DIVE
What happens at daemon startup
1. Read configplaywright MCP has --cdp-endpoint :9223
2. EnsureChromeDebugPortCheck if 9223 is reachable
3. If not → LaunchCDPChromeStart a dedicated Chrome instance
4. Clone profileCopy ~/Library/.../Chrome → ~/.shannon/chrome-cdp/
5. Launch minimized--remote-debugging-port=9223 + minimized window
6. Playwright connectsvia CDP instead of launching its own Chrome
Why clone the profile?
Your daily Chrome has all your logins (Gmail, Twitter, Notion, etc.). Playwright needs those logins to be useful, but:

Need to inherit cookies → copy profile
Must not interfere with your real Chrome → copy to isolated dir
Must not steal focus → launch minimized

The cloned profile gets logged-in state at the moment of cloning. It won't stay synced with your daily Chrome — but for agent use cases, that's fine.
Legacy tools get disabled
When Playwright MCP connects, ShanClaw removes these from the registry:

browser applescript accessibility screenshot wait_for

Why? Having both sets of tools causes LLMs to ping-pong between them — try Playwright, hit an error, fall back to applescript, fail, try screenshot, etc. This triggers the "tool mode switch" loop detector and wastes tokens. Force-disabling the legacy tools when Playwright is available eliminates the decision.
Real-world browser task cost
Tested: "check my Gmail inbox" with Playwright + CDP + cloned profile (already logged in)

Turn 1 (clarifying question): 8,123 tokens, $0.11, 12s
Turn 2 (actual inbox read): 19,269 tokens, $0.19, 25s
Total: 27,392 tokens, $0.29, 37s → gets ~15 subject lines from one screenshot

Each follow-up action (read body, reply, label) would cost another full cycle of navigate + snapshot + screenshot + file_read. This is why first-class email MCP is a much better fit.

9. Extensibility

Hooks
PreToolUseBlock tool (exit 2 = deny)
PostToolUseFire-and-forget logging
SessionStartSession initialization
StopCleanup on close
Timeout: 10s · Max output: 10KB · Recursion guard
Instructions (priority merge)
1. Global~/.shannon/instructions.md
2. Global rules~/.shannon/rules/*.md
3. Project.shannon/instructions.md
4. Project rules.shannon/rules/*.md
5. Local.shannon/instructions.local.md
Dedup by line · Higher priority wins · Max 16K chars
Skills (SKILL.md)
FormatYAML frontmatter + markdown body
Global~/.shannon/skills/<name>/
Attachedvia _attached.yaml manifest
BundledEmbedded in binary
MarketplaceInstall via API
Legacy agent-local skill directories are deprecated/cleaned up · invoked via use_skill tool
ShanClaw current codebase snapshot · Go 1.25.7 · Single binary · Open source
github.com/Kocoro-lab/ShanClaw