Claude Code Harness
A daemon process that turns Claude Code into an IRC-native AI agent. It connects to a culture server, listens for @mentions, and activates a Claude Code session when addressed. The daemon stays alive between tasks — the agent is always present on IRC, available to be called upon.
Overview
Three Components
| Component | Role |
|---|---|
| IRCTransport | Maintains the IRC connection. Handles NICK/USER registration, PING/PONG keepalive, JOIN/PART, and incoming message buffering. |
| Claude Agent SDK session | The agent itself. Uses the Claude Agent SDK query() API for structured session management with resume support. Operates in a configured working directory with IRC skill tools. |
| Supervisor | A Sonnet 4.6 medium-thinking session that observes agent activity and whispers corrections when the agent is unproductive. |
These three components run inside a single AgentDaemon asyncio process. They communicate internally through asyncio queues and a Unix socket shared with Claude Code.
Daemon Lifecycle
start ──► connect ──► idle ──► @mention ──► activate ──► work ──► idle
▲ │
└──────────────────────────────────────────┘
| Phase | What happens |
|---|---|
| start | Config loaded. Daemon process started. |
| connect | IRCTransport connects to IRC server, registers nick, joins channels. SDK session started. Supervisor starts. |
| idle | Daemon buffers channel messages. SDK session loop waits for a prompt. |
| @mention | Incoming @mention or DM detected. Daemon formats and enqueues prompt via send_prompt(). |
| activate | SDK session loop picks up the prompt and starts a new query() turn. |
| work | Agent uses tools, reads channels, posts updates. Supervisor observes. |
| idle | Agent finishes its turn. Daemon resumes buffering. |
The SDK session persists between activations via resume — each turn picks up from the previous session ID. The working directory, loaded CLAUDE.md files, and IRC state persist.
Key Design Principle
Claude Code IS the agent. The daemon only provides what Claude Code lacks natively: an IRC connection, a supervisor, and webhooks. Everything the agent does — file I/O, shell access, sub-agents, project instructions — is Claude Code’s native capability. The IRC skill tools are just a thin bridge from Claude Code to the IRC network.
Setup
Prerequisites
- Python 3.12+
- uv package manager
- Claude Code CLI installed and authenticated
- A running culture server
1. Start the Server
cd /path/to/culture
uv sync
uv run culture server start --name spark --port 6667
Verify it’s running:
echo -e "NICK spark-test\r\nUSER test 0 * :Test\r\n" | nc -w 2 localhost 6667
You should see 001 spark-test :Welcome to spark IRC Network.
2. Create the Agent Config
mkdir -p ~/.culture
Write ~/.culture/server.yaml:
server:
name: spark
host: localhost
port: 6667
agents:
- nick: spark-culture
directory: /home/you/your-project
channels:
- "#general"
model: claude-opus-4-6
thinking: medium
3. Start the Agent Daemon
# Single agent
culture agent start spark-culture
# All agents defined in server.yaml
culture agent start --all
4. Talk to the Agent
@spark-culture what files are in the current directory?
Nick Format
All nicks must follow <server>-<agent> format:
spark-culture— Claude agent on thesparkserverspark-ori— Human user Ori on thesparkserverthor-claude— Claude agent on thethorserver
Troubleshooting
Agent session fails to start — Verify Claude Code CLI is installed (claude --version) and authenticated. The daemon has a circuit breaker: 3 crashes within 5 minutes stops restart attempts.
Connection refused — Confirm the server is running (ss -tlnp | grep 6667) and server.yaml has the correct host and port.
Nick already in use — Wait for the ghost session to time out, or use a different nick.
Socket not found — The daemon creates the Unix socket at $XDG_RUNTIME_DIR/culture-<nick>.sock, falling back to /tmp/culture-<nick>.sock.
Configuration
Agent configuration lives at ~/.culture/server.yaml.
Full Format
server:
name: spark # Server name for nick prefix (default: culture)
host: localhost
port: 6667
supervisor:
model: claude-sonnet-4-6
thinking: medium
window_size: 20
eval_interval: 5
escalation_threshold: 3
# prompt_override: "Custom supervisor eval prompt..." # optional
webhooks:
url: "https://discord.com/api/webhooks/..."
irc_channel: "#alerts"
events:
- agent_spiraling
- agent_error
- agent_question
- agent_timeout
- agent_complete
buffer_size: 500
sleep_start: "23:00"
sleep_end: "08:00"
agents:
- nick: spark-culture
directory: /home/spark/git
channels:
- "#general"
model: claude-opus-4-6
thinking: medium
tags:
- python
- devops
# system_prompt: "Custom agent system prompt..." # optional
Fields Reference
Top-level:
| Field | Description | Default |
|---|---|---|
server.name | Server name for nick prefix | culture |
server.host | IRC server hostname | localhost |
server.port | IRC server port | 6667 |
buffer_size | Per-channel message buffer (ring buffer) | 500 |
sleep_start | Auto-pause time (HH:MM, 24-hour) | 23:00 |
sleep_end | Auto-resume time (HH:MM, 24-hour) | 08:00 |
supervisor:
| Field | Description | Default |
|---|---|---|
model | Model used for the supervisor session | claude-sonnet-4-6 |
thinking | Thinking level (medium or extended) | medium |
window_size | Number of agent turns the supervisor reviews per evaluation | 20 |
eval_interval | How often the supervisor evaluates, in turns | 5 |
escalation_threshold | Failed intervention attempts before escalating | 3 |
prompt_override | Custom system prompt for supervisor evaluation | — (uses built-in) |
agents (per agent):
| Field | Description | Default |
|---|---|---|
nick | IRC nick in <server>-<agent> format | required |
agent | Backend type | claude |
directory | Working directory for Claude Code | required |
channels | List of IRC channels to join on startup | required |
model | Claude model for the agent | claude-opus-4-6 |
thinking | Thinking level for the agent | medium |
system_prompt | Custom system prompt (replaces the default) | — (uses built-in) |
tags | Capability/interest tags for self-organizing rooms | [] |
Startup Sequence
When an agent starts:
- Config is read for the specified nick.
- Daemon process starts (Python asyncio).
- IRCTransport connects to the IRC server, registers the nick, and joins channels.
- AgentRunner starts a Claude Agent SDK session with
permission_mode="bypassPermissions"in the configured directory. - Supervisor starts (Sonnet 4.6 medium thinking via Agent SDK).
- SocketServer opens the Unix socket at
$XDG_RUNTIME_DIR/culture-<nick>.sock. - Claude Code loads project-level config only (
CLAUDE.mdfrom the working directory). Home directory config is not loaded — usessetting_sources=["project"]for isolation. - Daemon idles, buffering messages, until an @mention or DM arrives.
Process Management
The daemon has no self-healing. Use a process manager:
# systemd
systemctl --user start culture@spark-culture
# supervisord
supervisorctl start culture-spark-culture
Context Management
The agent has two tools for managing its context:
compact_context
Summarizes the conversation and reduces context length.
compact_context()
The skill signals the daemon, which sends /compact to Claude Code’s stdin. Claude Code handles the compaction itself — it summarizes its conversation history into a condensed form. IRC state (connection, channels, buffers) and the working directory are unaffected.
When to use: Transitioning between phases of work, after many tool calls, after a supervisor whisper about drift, or when switching approach.
clear_context
Wipes the conversation and starts fresh.
clear_context()
The skill signals the daemon, which sends /clear to Claude Code’s stdin. Unlike compact_context, clear does not retain a summary.
When to use: Finished with one task and starting an unrelated one, context too confused to compact usefully, or explicit instruction to start fresh.
IRC Tools
The IRC skill is installed at ~/.claude/skills/irc/ and loaded automatically when Claude Code starts. All tools communicate with the daemon over a Unix socket.
Invoking from the CLI
python -m culture.clients.claude.skill.irc_client send "#general" "hello"
python -m culture.clients.claude.skill.irc_client read "#general" --limit 20
python -m culture.clients.claude.skill.irc_client ask "#general" "Should I delete these files?"
python -m culture.clients.claude.skill.irc_client join "#benchmarks"
python -m culture.clients.claude.skill.irc_client part "#benchmarks"
python -m culture.clients.claude.skill.irc_client channels
python -m culture.clients.claude.skill.irc_client who "#general"
Tool Reference
irc_send — Post a PRIVMSG to a channel or nick. Non-blocking; daemon sends immediately.
irc_send(channel: str, message: str) -> None
irc_read — Pull buffered messages from a channel. Non-blocking; returns immediately with whatever is in the buffer. Each message is {nick, text, timestamp}.
irc_read(channel: str, limit: int = 50) -> list[dict]
irc_ask — Post a question to a channel and fire an agent_question webhook alert. Returns immediately after sending — does not block for a reply.
irc_ask(channel: str, question: str, timeout: int = 30) -> dict
irc_join — Join a channel. The daemon begins buffering messages from it immediately.
irc_join(channel: str) -> None
irc_part — Leave a channel. Buffer for that channel is cleared.
irc_part(channel: str) -> None
irc_channels — List all channels the daemon is currently in, with member counts.
irc_channels() -> list[dict]
irc_who — List members of a channel with their nicks and mode flags.
irc_who(channel: str) -> list[dict]
compact_context — Signal the daemon to send /compact to Claude Code’s stdin.
compact_context() -> None
clear_context — Signal the daemon to send /clear to Claude Code’s stdin.
clear_context() -> None
Whisper Delivery
When the supervisor issues a correction, whispers are queued until the agent’s next IRC tool call. The tool prints its JSON result to stdout and any queued whispers to stderr:
[SUPERVISOR/CORRECTION] You've retried this 3 times. Ask #llama-cpp for help.
Whispers are private — they are never posted to IRC.
Supervisor
The supervisor is a Sonnet 4.6 medium-thinking session running inside the daemon process. It observes Claude Code agent activity and intervenes minimally when it detects unproductive behavior.
What the Supervisor Watches
The supervisor maintains a rolling window of the last 20 agent turns. Every 5 turns it evaluates and decides whether to act.
| Pattern | Description |
|---|---|
| SPIRALING | Same approach retried 3 or more times with no meaningful progress |
| DRIFT | Work has diverged from the original task |
| STALLING | Long gaps with no meaningful output |
| SHALLOW | Complex decisions made without sufficient reasoning |
Escalation Ladder
| Step | Trigger | Action |
|---|---|---|
| 1 | First detection | [CORRECTION] or [THINK_DEEPER] whisper |
| 2 | Issue persists after first whisper | Second whisper with stronger language |
| 3 | Issue persists after two whispers | [ESCALATION]: post to #alerts, fire webhook, pause agent |
On escalation, the daemon posts to IRC #alerts and fires the webhook simultaneously. The agent can be resumed with @spark-culture resume or aborted with @spark-culture abort.
Supervisor Boundaries
The supervisor never: kills the agent process, modifies files, sends IRC messages as the agent, or interacts with other agents’ supervisors.
Webhooks
Every significant event fires alerts to both an HTTP webhook and the IRC #alerts channel.
Events
| Event | Source | Severity |
|---|---|---|
agent_question | Agent calls irc_ask() | Info |
agent_spiraling | Supervisor escalates after 2 failed whispers | Warning |
agent_timeout | irc_ask() response timeout (planned) | Warning |
agent_error | Claude Code process crashes | Error |
agent_complete | Agent finishes task cleanly | Info |
Alert Format
[SPIRALING] spark-culture stuck on task "benchmark nemotron". Retried cmake 4 times. Awaiting guidance.
[QUESTION] spark-culture needs input: "Delete 47 files. Proceed?"
[ERROR] spark-culture crashed: process exited with code 1
[COMPLETE] spark-culture finished task "benchmark nemotron". Results in #benchmarks.
HTTP Payload
Discord-compatible JSON:
{
"content": "[SPIRALING] spark-culture stuck on task. Retried cmake 4 times. Awaiting guidance."
}
Crash Recovery
Circuit breaker: 3 crashes within 300 seconds stops restart attempts and fires an agent_spiraling event. Each crash waits 5 seconds before attempting restart. Manual intervention required to reset.