Featured image of post The Complete Technical Routes for Hermes to Control Claude Code: From Terminal Invocation to Agent Interoperability Protocols

The Complete Technical Routes for Hermes to Control Claude Code: From Terminal Invocation to Agent Interoperability Protocols

How many ways can a Telegram bot gateway orchestrate Claude Code, Anthropic's CLI coding agent? This article surveys 7 technical routes — from direct terminal subprocess invocation, to SDK programmatic API, to MCP bidirectional communication, to A2A/ACP agent interoperability protocols — breaking down the principles, providing code examples, comparing them side by side, and giving practical recommendations based on Hermes's existing codex_app_server and copilot_acp runtime templates.

New post

One-Sentence Summary

Hermes (a Telegram bot gateway) controlling Claude Code (Anthropic’s CLI coding agent) has 7 technical routes: direct terminal invocation, SDK programmatic API, MCP bidirectional communication, A2A Agent-to-Agent protocol, ACP Agent Communication protocol, Hooks system, and Cloud sessions. Hermes already has codex_app_server and copilot_acp subprocess runtime templates — adding a Claude Code runtime is essentially copying an existing pattern. For Telegram bot scenarios, stream-json pipeline + canUseTool external permission approval + existing codex_app_server template is the most pragmatic route today.

Why Control Claude Code From External Systems

Claude Code is a terminal-based coding agent. You type claude, it enters an interactive session, reads your codebase, edits files, runs tests, commits git — all inside a TTY.

But often you don’t want to sit at the terminal:

  • You want to send a message from Telegram and have Claude Code fix a bug
  • You want a bot gateway (Hermes) to route multiple users’ messages to different Claude Code sessions
  • You want to trigger Claude Code in CI/CD pipelines for code review
  • You want one agent to invoke another (Agent-to-Agent collaboration)

The common thread: the control plane is not in a TTY — it needs a programmatic interface to drive Claude Code.

Hermes is NousResearch’s open-source agent framework — a Telegram bot gateway written in Python, routing messages through gateway/run.py to various LLMs. It already calls OpenAI, Anthropic, DeepSeek, and even has a codex_app_server runtime that hands entire conversation turns to the Codex CLI subprocess. The question: how to bring Claude Code in?

Claude Code v2.1.267 (the version at time of writing) offers more than one path. Let’s break them down.


Route 1: Direct Terminal Invocation

Principle: Claude Code is fundamentally a CLI binary. Any language that can spawn subprocesses can call it directly, communicating via stdin/stdout. This is the lowest-level route, and the foundation of all others — the SDK does essentially the same thing under the hood.

Hermes precedent: Hermes’s codex_app_server runtime already does this — spawning codex app-server as a subprocess, driving the entire conversation turn via JSON-RPC over stdio. The file agent/transports/codex_app_server.py fully implements this pattern. Adding a Claude Code runtime is essentially copying this template.

1.1 Headless Mode: -p / --print

1
2
3
4
5
6
7
8
# Simplest: give a prompt, get text back
claude -p "Explain what this code does" --output-format text

# Structured JSON (single result)
claude -p "List all TODO comments" --output-format json

# Streaming JSON (real-time, message by message)
claude -p "Refactor this function" --output-format stream-json

--output-format has three options (only in --print mode):

FormatUse caseCharacteristics
textSimple textOne-shot return, for short tasks
jsonStructured resultReturns one JSON object with the complete result
stream-jsonReal-time streamingEach message (thinking, tool calls, results) output incrementally, for long tasks and live monitoring

stream-json is critical for Telegram bot scenarios — you don’t want to wait 5 minutes for a complete result. You want to see “Claude is reading a file,” “Claude is editing code,” and push those progress updates to Telegram in real time.

1.2 Streaming Input: --input-format stream-json

1
2
# Bidirectional streaming: both stdin and stdout are stream-json
claude -p --input-format stream-json --output-format stream-json

This lets external systems send prompts as JSON messages via stdin incrementally, rather than all at once. You can have a continuous conversation within one Claude Code session — send a message, get a reply, send another.

With --include-partial-messages, you even get token-by-token partial messages — you can see Claude typing in real time.

1.3 Session Management

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Continue the most recent conversation
claude -c

# Resume a specific session
claude --resume <session-id>

# Fork a new session ID when resuming
claude --resume <session-id> --fork-session

# Resume from a PR
claude --from-pr 42

Session persistence is built in. Each session has a unique ID that can be resumed. This means Hermes can maintain independent Claude Code sessions per Telegram user — user A’s conversation doesn’t leak to user B.

1.4 Background Session Management

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Start a background session, return ID immediately
claude --bg "Fix the bug for me"

# List all background sessions (JSON, for scripting)
claude agents --json

# View recent output
claude logs f130ab53

# Attach to a background session (interactive)
claude attach f130ab53

# Stop a session (conversation preserved)
claude stop f130ab53

# Delete a session
claude rm f130ab53

--bg is a killer feature for Telegram bots. Hermes can: receive a user message, claude --bg to start a session, store the session ID, poll with claude logs or stream with --output-format stream-json back to Telegram, and stop to preserve the session when the user leaves.

1.5 Permission Control

This is the most critical issue for Telegram bot scenarios: Claude Code prompts for permission by default. In unattended bot scenarios, nobody is at the terminal to click “yes.”

Claude Code offers 6 permission modes:

ModeBehaviorUse case
manualAsk every timeInteractive, human at terminal
acceptEditsAuto-accept file edits, still ask for othersSemi-auto
autoSmart judgment on what’s safe to auto-executeSemi-auto
planRead-only, no modificationsCode review, security analysis
bypassPermissionsSkip all permission checksFull auto (high risk)
dontAskDon’t ask; deny anything that would promptSafest auto mode
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Safest auto: deny anything that would prompt
claude -p "Fix this bug" --permission-mode dontAsk

# External system approves permissions (SDK host or permission-prompt-tool)
claude -p "Fix bug" --permission-prompts host

# Whitelist specific tools
claude -p "Run tests" --allowedTools "Bash(npm test)" --permission-mode dontAsk

# Restricted mode: remove all command-executing tools
claude -p "Analyze code" --restricted

--permission-prompts host is the bridge — it allows an external system (via SDK) to answer permission questions. This is exactly what Telegram bot scenarios need: Hermes receives Claude Code’s permission request, forwards it to the user in Telegram to click “allow” or “deny.”

1.6 Subprocess Call Example (Python, Hermes-style)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import subprocess, json

def claude_stream(prompt: str, session_id: str = None, cwd: str = "."):
    """Start a Claude Code streaming session, yield messages.
    Structurally identical to Hermes's codex_app_server runtime."""
    cmd = [
        "claude", "-p", prompt,
        "--output-format", "stream-json",
        "--input-format", "stream-json",
        "--permission-mode", "dontAsk",
        "--allowedTools", "Bash(git *) Edit Read",
    ]
    if session_id:
        cmd.extend(["--resume", session_id])

    proc = subprocess.Popen(
        cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
        stderr=subprocess.PIPE, cwd=cwd, text=True
    )

    for line in proc.stdout:
        msg = json.loads(line)
        msg_type = msg.get("type", "")
        if msg_type == "assistant":
            for block in msg.get("message", {}).get("content", []):
                if block.get("type") == "text":
                    yield {"type": "text", "content": block["text"]}
                elif block.get("type") == "tool_use":
                    yield {"type": "tool", "name": block["name"]}
        elif msg_type == "result":
            yield {"type": "done", "cost": msg.get("cost_usd")}
            break
    proc.wait()

# Hermes invocation
for event in claude_stream("Fix the null pointer exception in auth module"):
    if event["type"] == "text":
        send_to_telegram(user_id, event["content"])

Route 2: Claude Code SDK (Programmatic API)

Principle: Anthropic wraps a query() function in the @anthropic-ai/claude-code npm package, encapsulating process management, message parsing, streaming output, and permission approval into an API layer. The SDK and direct terminal invocation share the same foundation — both spawn the claude subprocess — but the SDK wraps all the plumbing.

2.1 TypeScript/JavaScript SDK

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { query, type Message } from "@anthropic-ai/claude-code";

const result = await query({
  prompt: "Fix the auth module bug",
  options: {
    model: "claude-sonnet-5",
    permissionMode: "dontAsk",
    allowedTools: ["Bash(git *)", "Edit", "Read"],
    cwd: "/path/to/project",
  },
});

for await (const message of result) {
  switch (message.type) {
    case "assistant":
      console.log(message.message.content);
      break;
    case "result":
      console.log(`Cost: $${message.cost_usd}`);
      break;
  }
}

2.2 Python SDK

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from claude_code_sdk import query, ClaudeCodeOptions

async def run_claude(prompt: str):
    options = ClaudeCodeOptions(
        model="claude-sonnet-5",
        permission_mode="dontAsk",
        allowed_tools=["Bash(git *)", "Edit", "Read"],
    )
    async for message in query(prompt=prompt, options=options):
        if message.type == "assistant":
            print(message.content)

2.3 canUseTool Callback — External Permission Approval

This is the SDK’s biggest advantage over raw subprocess calls:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const result = await query({
  prompt: "Refactor the database migration script",
  options: {
    permissionMode: "dontAsk",
    canUseTool: async (toolName, input) => {
      // Hermes forwards the permission request to Telegram
      const approved = await ask_user_in_telegram(
        `Claude wants to execute ${toolName}:\n${JSON.stringify(input)}\nAllow?`
      );
      return approved ? "allow" : "deny";
    },
  },
});

canUseTool turns Hermes into a permission approval proxy: every time Claude Code wants to use a tool, it asks Hermes first, and Hermes asks the Telegram user. This is far more granular than bypassPermissions (allow all) or dontAsk (deny all).

2.4 Complete Type Definitions

The SDK ships with sdk-tools.d.ts, covering all built-in tool types — AgentInput, BashInput, FileEditInput, McpInput, WorkflowInput, CronCreateInput, and 30+ more. External systems can construct and parse tool calls type-safely.

2.5 SDK vs Direct Terminal Invocation

DimensionDirect terminalSDK
Process managementManual spawn/waitAutomatic
Message parsingManual JSON.parseTyped objects
Permission approvalNo canUseTool✅ Callback-based
Error handlingManual exit code checksBuilt-in retry and error types
LanguageAnyTS/JS, Python

Conclusion: If Hermes’s component is Node or Python, use the SDK. To embed in Hermes’s Python gateway, you can follow the codex_app_server subprocess pattern and wrap it yourself — because Hermes’s transport layer already has mature subprocess JSON-RPC infrastructure.


Route 3: MCP (Model Context Protocol)

Principle: MCP is Anthropic’s open standard — letting AI models connect to external tools and data sources. Claude Code can be both an MCP client (connecting to others’ servers) and an MCP server (exposing its own capabilities).

Hermes already has this: Hermes has mcp_serve.py — an MCP server exposing Telegram conversations as tools, letting any MCP client (Claude Code, Cursor, Codex) read message history, send messages, and manage approvals. Meanwhile, tools/mcp_tool.py can already spawn claude mcp serve as a subprocess.

3.1 Claude Code as MCP Server

1
claude mcp serve

This turns Claude Code into an MCP server, exposing code editing, command execution, and file I/O via stdio. Any MCP client can connect.

3.2 Claude Code as MCP Client

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Add stdio MCP server
claude mcp add my-server -- python /path/to/server.py

# Add HTTP MCP server
claude mcp add --transport http hermes-api https://hermes.example.com/mcp

# With authentication
claude mcp add --transport http hermes-api https://hermes.example.com/mcp \
  --header "Authorization: Bearer <token>"

# From JSON
claude mcp add-json hermes-server '{"type":"stdio","command":"python","args":["server.py"]}'

# List/get/login/remove
claude mcp list
claude mcp get hermes-api
claude mcp login hermes-api
claude mcp remove hermes-api

3.3 Four Combinations

MCP is the only bidirectional peer-to-peer route — both Hermes and Claude Code can be server or client:

Combination A: Hermes as Server, Claude Code as Client Hermes implements an MCP server exposing Telegram tools (send/read messages). Claude Code connects via claude mcp add and can directly send/receive Telegram messages in code. This is the direction Hermes’s mcp_serve.py already implements.

Combination B: Claude Code as Server, Hermes as Client Claude Code runs claude mcp serve to expose coding capabilities. Hermes connects as an MCP client, calling Claude Code’s tools like any other tool. This is what Hermes’s tools/mcp_tool.py already supports.

Combination C: Both as Servers Both run MCP servers, interconnected. For complex collaboration scenarios.

3.4 Isolation Control

1
2
# Only use servers from specified config
claude -p "Analyze code" --mcp-config hermes-only.json --strict-mcp-config

--strict-mcp-config ensures Claude Code only connects to your specified servers — preventing session cross-contamination in multi-tenant scenarios.

3.5 MCP’s Position

MCP is suited for capability bridging (letting Claude Code use Hermes’s Telegram tools, or letting Hermes use Claude Code’s coding tools), but not for complete turn handoff — MCP operates at the tool-call level, not the session management level. For full turn handoff, use Route 1’s subprocess mode or Route 5’s ACP.


Route 4: A2A (Agent-to-Agent Protocol)

Principle: A2A is an open protocol announced by Google in April 2025, designed to let AI agents built on different frameworks (LangChain, CrewAI, AutoGen, etc.) discover each other and collaborate. It is now governed by the Linux Foundation, with 50+ tech companies backing it (Salesforce, SAP, Atlassian, Microsoft).

Think of it as HTTP for agents: just as HTTP lets browsers and servers from different vendors talk to each other, A2A lets agents from different vendors talk to each other.

4.1 Three-Layer Architecture

LayerContent
Data ModelTask, Message, AgentCard, Part, Artifact
OperationsSend Message, Send Streaming Message, Get/Cancel Task, Get Agent Card
Protocol BindingsJSON-RPC 2.0 (primary), gRPC, HTTP/REST

4.2 Core Mechanisms

Agent Card discovery: Each agent publishes a JSON metadata document at /.well-known/agent.json, describing its identity, capabilities, skills, supported I/O modalities, and authentication.

1
2
3
4
5
6
7
8
{
  "name": "claude-code-agent",
  "description": "Anthropic's CLI coding agent",
  "url": "https://hermes.example.com/claude-code",
  "capabilities": { "streaming": true, "pushNotifications": true },
  "skills": [{ "id": "code-review", "name": "Code Review" }],
  "authentication": { "schemes": ["bearer"] }
}

JSON-RPC over HTTPS: All communication uses JSON-RPC 2.0. Send Streaming Message uses SSE for real-time streaming.

Task lifecycle: submitted → working → input-required → completed/canceled/failed. An agent delegates a task to another agent, then polls or streams for results.

1
2
3
4
5
6
7
8
{
  "jsonrpc": "2.0", "id": 1,
  "method": "tasks/send",
  "params": {
    "id": "task-123",
    "message": { "role": "user", "parts": [{"type":"text","text":"Review this code"}] }
  }
}

4.3 Claude Code Support Status

Not natively supported. Claude Code does not implement an A2A server or client. But three viable paths exist:

  1. Anthropic has demonstrated it: A webinar (“Deploying Multi-Agent Systems using MCP and A2A with Claude on Vertex AI”) shows MCP (agent-to-tools) + A2A (agent-to-agent) + Claude combined.

  2. A2A-MCP Bridge (community): GitHub’s GongRzhe/A2A-MCP-Server is an MCP server bridging A2A and MCP. Since Claude Code supports MCP, this bridge lets Claude Code participate in A2A workflows indirectly — Claude Code can call A2A-exposed agents as MCP tools, and vice versa.

  3. python-a2a library: PyPI’s python-a2a is a complete A2A client/server library that can wrap Claude Code’s CLI as an A2A server.

4.4 How Hermes Could Use A2A to Control Claude Code

Approach A: Wrap Claude Code as an A2A server

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from python_a2a import A2AServer, AgentCard, Skill
import subprocess, json

class ClaudeCodeA2A(A2AServer):
    @property
    def agent_card(self):
        return AgentCard(
            name="claude-code",
            description="Anthropic's CLI coding agent",
            skills=[Skill(id="coding", name="Code Generation & Editing")],
        )

    def handle_message(self, message):
        prompt = message.parts[0].text
        result = subprocess.run(
            ["claude", "-p", prompt, "--output-format", "json",
             "--permission-mode", "dontAsk"],
            capture_output=True, text=True
        )
        return json.loads(result.stdout)

Hermes sends A2A tasks/send → server spawns Claude Code → returns result as task artifact. Standardized discovery, streaming (SSE), and task lifecycle all free.

Approach B: Use the A2A-MCP Bridge

Deploy the A2A-MCP Bridge as an MCP server that Claude Code connects to. Hermes acts as an A2A client, delegating tasks to the bridge, which translates them to MCP tool calls that Claude Code processes. Indirect but requires no custom server code.

4.5 A2A’s Position

A2A is suited for cross-vendor multi-agent orchestration — if Hermes needs to control not just Claude Code but also Gemini CLI, Copilot CLI, and other agents, A2A provides a unified interface. But for controlling just Claude Code alone, A2A’s protocol overhead isn’t worth it — direct terminal invocation is simpler.

References:


Route 5: ACP (Agent Communication Protocol)

Principle: ACP (Agent Client Protocol / Agent Communication Protocol) is a standardized inter-agent communication protocol, using JSON-RPC over stdio to let one agent hand a conversation turn to another.

Hermes already has a complete implementation — this route’s biggest differentiator: it’s not theoretical, it’s code already running in Hermes:

  • agent/copilot_acp_client.py: spawns copilot --acp subprocess, ACP JSON-RPC, “forwards Hermes requests to copilot –acp”
  • acp_adapter/: exposes Hermes itself as an ACP agent, letting editors like Zed connect via hermes acp
  • agent/transports/codex_app_server.py: a similar subprocess JSON-RPC pattern (the codex version), serving as a sibling template for the ACP runtime

In other words, adding a Claude Code ACP runtime is essentially copying the copilot_acp_client.py pattern — replacing copilot --acp with claude (if Claude Code supports ACP), or with claude -p --input-format stream-json --output-format stream-json (using stream-json instead of ACP JSON-RPC).

5.0 Two ACPs

“ACP” has two meanings in the AI agent ecosystem, both relevant to Claude Code:

ACP #1: Agent Communication Protocol (IBM)

An open standard by IBM Research (2025), originally built for the BeeAI platform. REST-native design (not JSON-RPC), supporting multi-modal messages (text, files, structured data), async streaming, and Agent Identity (AID). Merged into A2A in late 2025 under the Linux Foundation — the official site agentcommunicationprotocol.dev states “ACP is now part of A2A.”

So if you hear “ACP protocol” in the context of agent interoperability, it’s basically part of A2A now.

ACP #2: Agent Client Protocol (Zed Industries / JetBrains)

This is the more practically relevant one. Released August 2025 by Zed Industries (co-developed with JetBrains and Google), it’s positioned as “the LSP for AI coding agents” — just as the Language Server Protocol standardized editor-to-language-server communication, ACP standardizes editor-to-AI-agent communication.

Architecture:

  • JSON-RPC 2.0 over stdin/stdout (identical transport to LSP)
  • Agent as server (subprocess), editor as client
  • Lifecycle methods: InitializeLoadConfigurationNewConversation / PromptConversationUpdated (streaming notification) → CancelConversationShutdown
  • 25+ agents support ACP, including Google Gemini CLI, GitHub Copilot CLI, Cursor, and others
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// ACP handshake
{
  "jsonrpc": "2.0", "id": 1,
  "method": "initialize",
  "params": { "clientInfo": { "name": "hermes", "version": "1.0" } }
}

// Start a conversation
{
  "jsonrpc": "2.0", "id": 2,
  "method": "newConversation",
  "params": { "prompt": "Fix the bug in the auth module" }
}

// Agent pushes progress via streaming
{ "method": "conversationUpdated", "params": { "content": "Reading auth.py..." } }

Claude Code support status:

  • Not natively supported — GitHub issue #6686 is a community feature request; Anthropic has not officially implemented the ACP server side
  • But JetBrains integration works — JetBrains IDEs (IntelliJ IDEA, WebStorm, etc.) added ACP support in their AI Assistant plugin, and you can configure Claude Code CLI as a custom ACP agent. The IDE spawns claude as a subprocess and communicates via stdin/stdout JSON-RPC. This works because Claude Code’s CLI already supports stdin/stdout I/O, which is close enough to ACP’s expected behavior

5.1 Feasibility of Hermes as an ACP Client

Hermes could play a “virtual editor” — an ACP client:

  1. Spawn claude as a subprocess
  2. Send JSON-RPC initializeloadConfigurationnewConversationprompt over stdin
  3. Read streaming conversationUpdated notifications from stdout
  4. Use cancelConversation to interrupt long-running tasks
  5. Use readResource / editResource for file operations

This gives Hermes structured, protocol-level control of Claude Code with proper lifecycle management, cancellation, and streaming. However, Claude Code doesn’t officially implement the ACP server side — the JetBrains integration works because Claude Code’s stdin/stdout is close enough to ACP’s behavior.

Practical assessment: ACP is primarily designed for IDE-to-agent communication, not bot-to-agent. For Telegram bot scenarios, ACP’s stdin/stdout JSON-RPC pattern is architecturally very similar to direct terminal invocation’s stream-json — but stream-json is natively supported by Claude Code, while ACP requires implementing the client side. If you’re only doing bot integration, direct terminal invocation is simpler; if you’re building a general agent access layer (supporting multiple editors/agents), ACP is worth the investment.

References:

5.2 Side-by-Side: A2A vs ACP vs Direct Terminal Invocation

DimensionA2AACP (Agent Client)Direct Terminal
TransportJSON-RPC over HTTPS + SSEJSON-RPC over stdin/stdoutSubprocess stdin/stdout
MaturitySpec final, 50+ backersSpec stable, 25+ agentsProduction-ready, official docs
Claude Code native❌ (needs bridge/wrapper)❌ (works via JetBrains)✅ fully native
Hermes integration costMedium-High (build A2A server)Medium (implement ACP client)Low (subprocess + JSON parse)
Best forCross-vendor multi-agentIDE integration, editor-agnosticBot/automation control of single CC
StreamingSSE (HTTP)stdin/stdout notificationsstream-json (real-time)
Session managementTask lifecycleConversation lifecycle–resume/–continue

Conclusion: Direct terminal invocation is the clear winner for immediate implementation — native, documented, no protocol implementation needed. A2A is the right choice for orchestrating multiple different agents. ACP is primarily an IDE concern and less relevant for a Telegram bot gateway, though its stdin/stdout JSON-RPC pattern is architecturally similar to what stream-json already provides.

5.3 Hermes’s ACP Implementation Pattern

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Pattern from agent/copilot_acp_client.py (simplified)
import subprocess, json

class ACPRuntime:
    """ACP runtime: spawn CLI subprocess, JSON-RPC over stdio"""
    def __init__(self, command: list[str]):
        self.proc = subprocess.Popen(
            command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
            stderr=subprocess.PIPE, text=True
        )

    def initialize(self):
        """ACP handshake"""
        return self._rpc("initialize", {"protocolVersion": "1.0"})

    def start_turn(self, messages: list, tools: list):
        """Hand the conversation turn to the subprocess agent"""
        return self._rpc("turn/start", {"messages": messages, "tools": tools})

    def consume_stream(self):
        """Consume streaming events until turn/completed"""
        for line in self.proc.stdout:
            event = json.loads(line)
            yield event
            if event.get("method") == "turn/completed":
                break

# Claude Code version (if CC supports ACP):
# runtime = ACPRuntime(["claude", "--acp"])
# Or using stream-json instead:
# runtime = ACPRuntime(["claude", "-p", "--input-format", "stream-json",
#                       "--output-format", "stream-json"])

5.4 Hermes’s codex_app_server Template

The codex_app_server runtime is a more complete reference — it doesn’t just do ACP, it hands the entire conversation turn to the Codex CLI:

1
2
3
4
5
6
Gateway receives Telegram message
  → conversation_loop.py detects api_mode == codex_app_server
  → bypasses default Hermes path, spawns codex app-server
  → initialize handshake → thread/start → turn/start
  → consumes streaming item/* notifications until turn/completed
  → projects events into Hermes display and transcript

Key files:

  • agent/transports/codex_app_server.py: spawn + JSON-RPC + streaming consumption
  • codex_app_server_session.py: session management
  • codex_event_projector.py: projects subprocess events to Hermes UI/transcript
  • agent/codex_runtime.py::run_codex_app_server_turn(): forwarder from run_agent.py

Path to add a Claude Code runtime: Copy these files, replace codex app-server with claude -p --input-format stream-json --output-format stream-json, replace JSON-RPC handshake with stream-json message protocol. OAuth credentials are already solved — anthropic_adapter.py::read_claude_code_credentials() can read Claude Code’s OAuth token.


Route 6: Hooks System (Bidirectional Hooks)

Principle: Claude Code’s Hooks system triggers custom scripts before/after tool execution and at session end. This isn’t “controlling” Claude Code — it’s Claude Code proactively notifying external systems.

DimensionCommand-driven (terminal/SDK/MCP)Event-driven (Hooks)
DirectionExternal → Claude CodeClaude Code → External
TriggerExternal systemClaude Code itself
Use caseDrive Claude Code to do workMonitor/intercept Claude Code’s behavior

6.1 Three Hook Types

  • PreToolUse: Triggers before tool execution. Can intercept — non-zero exit code blocks execution.
  • PostToolUse: Triggers after tool execution. For notification.
  • Stop: Triggers at session end. For cleanup.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{"type": "command", "command": "python /opt/hermes/hooks/pre_tool.py"}]
    }],
    "PostToolUse": [{
      "matcher": ".*",
      "hooks": [{"type": "command", "command": "python /opt/hermes/hooks/post_tool.py"}]
    }],
    "Stop": [{
      "hooks": [{"type": "command", "command": "python /opt/hermes/hooks/on_stop.py"}]
    }]
  }
}

6.2 Hermes via Hooks: Telegram Real-time Notification

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# /opt/hermes/hooks/post_tool.py — push tool execution notification to Telegram
import sys, json, requests

event = json.loads(sys.stdin.read())
tool = event["tool_name"]
inp = event.get("tool_input", {})

msg = f"🔧 Claude Code executed {tool}"
if tool == "Edit": msg += f"\nFile: {inp.get('file_path')}"
elif tool == "Bash": msg += f"\nCommand: {inp.get('command', '')[:100]}"

requests.post("https://api.telegram.org/bot<TOKEN>/sendMessage",
              json={"chat_id": CHAT_ID, "text": msg})
1
2
3
4
5
6
7
8
9
# /opt/hermes/hooks/pre_tool.py — security interception
import sys, json

event = json.loads(sys.stdin.read())
cmd = event.get("tool_input", {}).get("command", "")
dangerous = ["rm -rf /", "DROP TABLE", "git push --force"]
if any(d in cmd for d in dangerous):
    print(json.dumps({"decision": "block", "reason": f"Dangerous command blocked: {cmd}"}))
    sys.exit(2)  # Non-zero exit = block execution

Hooks are not a standalone route — they’re a supplement. Best practice: SDK/subprocess drives work (command-driven) + Hooks monitors behavior (event-driven) + MCP bridges capabilities (protocol layer), combined.


Route 7: Cloud Sessions

Principle: Claude Code supports creating sessions in Anthropic’s cloud — not running locally.

1
2
3
4
5
6
7
8
9
# Create a cloud session
claude --cloud "Help me refactor the auth module"

# Attach via session ID or claude.ai/code URL
claude --cloud <session-id>
claude --cloud https://claude.ai/code/sessions/abc123

# On a self-hosted environment
claude --environment ccpool_xxx "Analyze code security"

Significance: Hermes can create cloud sessions from a VPS — users chat in Telegram, computation happens in Anthropic’s cloud — the VPS doesn’t even need Claude Code installed.

1
Telegram user → Hermes (VPS) → claude --cloud → Cloud Session (Anthropic cloud) → GitHub repo

Advantages: No local install, no local code repo, natural multi-tenancy. Disadvantages: Depends on cloud availability, coarse control granularity, not suited for local filesystem tasks.


Side-by-Side Comparison

RouteComplexityReal-timeBidirectionalAutonomySecurityHermes readiness
Direct terminalLowHigh (stream-json)HighMedium (needs perm mode)✅ codex_app_server template
SDKLowHighHighHigh (canUseTool)⚠️ needs wrapping
MCPMediumMedium (req-resp)MediumMedium✅ mcp_serve.py + mcp_tool.py
A2AHighMediumHighMedium❌ not implemented
ACPMediumHighHighMedium✅ copilot_acp_client.py
HooksLowHigh (event-driven)One-way (CC→ext)N/AHigh (can intercept)⚠️ needs config
CloudLowestMediumOne-wayHighLow❌ not implemented

Practical Recommendation: Best Approach for Hermes + Claude Code

Based on Hermes’s actual architecture and existing infrastructure, a three-step approach:

Step 1: MCP Interconnection (Already exists, zero cost)

Hermes’s mcp_serve.py already exposes Telegram conversations as MCP tools. Claude Code can directly claude mcp add to connect to Hermes, proactively sending/receiving Telegram messages during coding. Zero development cost — already usable.

Meanwhile, Hermes’s tools/mcp_tool.py can already spawn claude mcp serve as a subprocess, letting Hermes proactively call Claude Code’s file operation tools. Also zero cost.

Use case: Lightweight collaboration — Claude Code needs to notify Telegram users, or Hermes needs to invoke Claude Code’s file operations.

Step 2: Subprocess Runtime (Copy codex_app_server template)

This is the core integration — letting Hermes hand entire conversation turns to Claude Code, just as it currently does with Codex.

Implementation path:

  1. Copy agent/transports/codex_app_server.pyagent/transports/claude_code.py
  2. Replace codex app-server command with claude -p --input-format stream-json --output-format stream-json
  3. Replace JSON-RPC handshake with stream-json message protocol
  4. Copy codex_app_server_session.pyclaude_code_session.py
  5. Copy codex_event_projector.pyclaude_event_projector.py (map codex events to Hermes UI)
  6. Add api_mode == "claude_code" branch in conversation_loop.py
  7. Add model.anthropic_runtime: "claude_code" switch in config.yaml
  8. OAuth credentials already solved — anthropic_adapter.py::read_claude_code_credentials() is reusable

Estimated effort: 2-3 days, because all infrastructure (subprocess management, event projection, session management, credential reading) already exists.

1
2
3
4
5
6
# config.yaml addition
model:
  default: claude-sonnet-5
  provider: anthropic
  anthropic_runtime: "claude_code"  # New: hand off conversation turns
  api_mode: claude_code_stream      # New

Step 3: ACP Runtime (Optional, using existing acp template)

If Claude Code supports ACP in the future (not currently confirmed), copy the copilot_acp_client.py pattern to add an ACP-version Claude Code runtime. This would be more standardized than stream-json, but requires Claude Code to expose an ACP interface.

Permission Strategy

  • Dev/testing: --permission-mode dontAsk (deny what should be denied, safest)
  • Production: --permission-prompts host + SDK canUseTool callback (forward to Telegram inline keyboard for user approval)
  • Never use: --dangerously-skip-permissions in multi-tenant Telegram bot scenarios

Complete Architecture

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Telegram user
     message
Hermes gateway (Python, /opt/hermes)
     api_mode == claude_code
     spawn: claude -p --input-format stream-json --output-format stream-json
Claude Code subprocess (stream-json bidirectional pipe)
     --permission-prompts host  canUseTool callback
     Hermes forwards permission request to Telegram inline keyboard
     User clicks "Allow"  canUseTool returns "allow"
Claude Code executes tools (Edit/Bash/Read...)
     PostToolUse hook  push notification to Telegram
     stream-json events  claude_event_projector  Hermes transcript
     turn/completed  reply pushed to Telegram

Conclusion

Claude Code was designed from the start with multiple layers of programmatic interfaces. From the lowest-level subprocess pipe, to the wrapped SDK, to the standardized MCP protocol, to the emerging A2A/ACP agent interoperability protocols, each route solves problems at a different level:

  • Drive Claude Code to do work → SDK or direct terminal invocation (subprocess)
  • Approve Claude Code’s operations → SDK’s canUseTool callback + --permission-prompts host
  • Expose capabilities to Claude Code → MCP Server (Hermes’s mcp_serve.py already exists)
  • Let Claude Code use your tools → MCP Client + claude mcp add (tools/mcp_tool.py already exists)
  • Monitor Claude Code’s behavior → Hooks
  • Cross-agent collaboration → A2A / ACP (copilot_acp_client.py already has the ACP template)
  • Don’t want to run locally → Cloud Sessions

For a Telegram bot gateway like Hermes, the good news is: most infrastructure already exists. The codex_app_server runtime provides a complete subprocess session-handoff template, copilot_acp provides the ACP protocol template, mcp_serve/mcp_tool provides MCP bidirectional communication, and anthropic_adapter provides OAuth credential reading. Adding a Claude Code runtime is essentially copying an existing pattern — it’s not building wheels from scratch, it’s plugging a new transport into a proven architecture.

Agent interoperability protocols (A2A, ACP) are the more future-oriented direction — when every AI tool exposes a standardized interface, “control” will no longer require subprocess pipes and SDK wrappers; it’ll be a protocol handshake. But until that day arrives, the stream-json pipe + canUseTool permission approval + existing runtime templates are the most pragmatic route.