Migration Guide: OpenClaw to Claude Code
=========================================

Written April 2026. Based on a real migration -- not a vendor doc.

If you ran agents through OpenClaw's gateway and need to rebuild, this is the
practical guide. It covers personal assistant bots, chat platform bots,
cron-based agents, and multi-agent setups. What transfers, what doesn't, and
what you have to build yourself.


What OpenClaw Gave You
----------------------

OpenClaw was more than a token proxy. It was an ecosystem. Here's what it
provided and what replaces it (or doesn't).

Token proxy (OAuth subscription -> API calls):
  Claude Code CLI replaces this natively. You authenticate with your Max
  subscription via `claude auth login`. No API key, no third-party token
  exchange. This part is a clean replacement.

Gateway (WebSocket server, persistent process):
  No direct equivalent. OpenClaw maintained a persistent server that your
  bots connected to. Now you manage your own persistence. Your bot is a
  process you run, and it spawns `claude` as a subprocess.

Session management (multi-turn conversations):
  OpenClaw tracked conversations at the gateway level. Now you build this
  yourself using --session-id and --resume flags. It works, but it's your
  code managing the state.

Scheduled tasks (heartbeats, crons):
  OpenClaw had internal scheduling. Now you use system cron + `claude -p`.
  Functionally equivalent once set up. Actually more transparent -- you can
  see exactly what runs when.

A2A messaging (agent-to-agent):
  OpenClaw had gateway-mediated A2A. This is genuinely degraded. You build
  file-based or socket-based workarounds. See the A2A section below.

Multi-workspace agents:
  OpenClaw managed separate agent configs. Now you separate agents by
  working directory, each with its own CLAUDE.md. Simpler in some ways,
  but you handle the separation yourself.

Web UI / control panel:
  Gone. Not replaced. You manage everything via CLI, cron, and logs.


Core Concept: Claude Code CLI as the Engine
-------------------------------------------

Claude Code is Anthropic's official CLI tool. It authenticates with your Max
subscription, gives Claude full tool access (file I/O, shell commands, web
search), and reads configuration from the working directory.

Install:

  curl -fsSL https://claude.ai/install.sh | sh

Authenticate:

  claude auth login

This opens a browser. Log in with your Max subscription. The credential lives
on the machine. Done.

Three invocation patterns cover every use case:

1. Interactive sessions (Telegram bot, chat platform bot):
   Use a wrapper like claude-code-telegram, or build your own connector that
   spawns claude as a subprocess per conversation.

2. One-shot tasks (cron jobs, background agents):
   claude -p "your prompt here" --permission-mode bypassPermissions --output-format text
   Runs, produces output, exits. No session state.

3. Persistent sessions (multi-turn conversations):
   Create: claude --session-id <uuid> -p "first message" --permission-mode bypassPermissions --output-format text
   Resume: claude --resume <uuid> -p "next message" --permission-mode bypassPermissions --output-format text
   Full context retained across turns. Session persists until rotated.


CLAUDE.md as System Prompt
--------------------------

This is the key concept. Claude Code reads a file called CLAUDE.md from
whatever directory you run it in. That file is your system prompt.

No more embedding prompts in code, no escaping issues, no string literals.
You write a markdown file. Claude reads it. That's the configuration.

For multi-agent setups: each agent runs from its own directory with its own
CLAUDE.md. The agent that runs from /home/you/bots/telegram-bot/ reads
/home/you/bots/telegram-bot/CLAUDE.md. The agent that runs from
/home/you/bots/irc-bot/ reads /home/you/bots/irc-bot/CLAUDE.md.

Working directory is identity. This is how you keep agents separate.

One subtlety: if you run interactive sessions (like a Telegram bot wrapper),
the bot process might run from a different directory than your agent's
workspace. You need a CLAUDE.md in whatever directory the bot actually
executes from. That file can be a pointer:

  # CLAUDE.md -- Entry Point

  Your workspace is /path/to/your/agent/workspace. Read the full CLAUDE.md
  and startup files from there.

  ## Session Startup
  1. Read /path/to/workspace/SOUL.md
  2. Read /path/to/workspace/MEMORY.md
  3. Read /path/to/workspace/memory/YYYY-MM-DD.md for today
  ...

This is the pattern we use. The home directory CLAUDE.md redirects to the
real workspace. Cron scripts cd into the workspace before running claude, so
they pick up the right CLAUDE.md directly.


The Workspace Pattern
---------------------

Every agent -- regardless of platform -- benefits from a workspace directory.
This is where identity, memory, and state live. The pattern is
platform-agnostic.

Minimal workspace:

  workspace/
    CLAUDE.md       -- behavior rules, instructions, tool paths
    SOUL.md         -- core identity. Who the agent is.
    MEMORY.md       -- long-term curated memory
    HEARTBEAT.md    -- active tasks, pending items
    memory/
      YYYY-MM-DD.md -- daily logs (one file per day)

Extended workspace (for richer agents):

  workspace/
    CLAUDE.md
    SOUL.md
    IDENTITY.md     -- sense of self, updated when something fundamental shifts
    MEMORY.md
    HEARTBEAT.md
    USER.md         -- about the operator
    RELATIONSHIPS.md -- who the agent knows
    NOW.md          -- current state, active threads
    memory/
      YYYY-MM-DD.md
      infrastructure.md  -- topic-specific memory indices
      projects.md
      lessons.md

The daily memory files are critical. They're how the agent survives restarts.
Without them, every session is amnesia. With them, the agent picks up where
it left off.

On session start, your code (or your CLAUDE.md startup instructions) tells
the agent to read these files. On resume, Claude already has them in context.

You can also have the agent write to its own memory during a session. We use
inline markers like [WRITE_MEMORY: thing to remember] that the platform
wrapper strips from the output and appends to the day file. The agent builds
its own continuity.


Platform Layer: What You Build Yourself
---------------------------------------

OpenClaw handled the connection to your chat platform. Now you do. The
general pattern is the same regardless of platform.

Your platform connector does five things:

  1. Connects to the platform (Telegram, Discord, IRC, FSC2, Slack, etc.)
  2. Receives messages
  3. Calls claude as a subprocess (one-shot or via session manager)
  4. Parses output for any markers you've defined (memory writes, PMs, etc.)
  5. Sends response back to the platform

In Python, the core of this is subprocess.run:

  import subprocess
  import uuid

  session_id = str(uuid.uuid4())

  # First message -- creates session
  result = subprocess.run(
      ["claude", "-p", prompt,
       "--session-id", session_id,
       "--permission-mode", "bypassPermissions",
       "--output-format", "text"],
      capture_output=True, text=True, timeout=90,
      cwd="/path/to/your/agent"
  )
  reply = result.stdout.strip()

  # Subsequent messages -- resumes session
  result = subprocess.run(
      ["claude", "-p", next_prompt,
       "--resume", session_id,
       "--permission-mode", "bypassPermissions",
       "--output-format", "text"],
      capture_output=True, text=True, timeout=90,
      cwd="/path/to/your/agent"
  )

The cwd parameter controls which CLAUDE.md gets loaded. The timeout prevents
your bot from hanging if Claude Code stalls.


Session Management
------------------

If your bot has multi-turn conversations, you need a session manager. This
handles creation, resumption, rotation, and error recovery.

Here's the pattern, adapted from a working implementation:

  import threading
  import uuid
  import subprocess

  SESSION_TIMEOUT = 90
  SESSION_MAX_TURNS = 200

  class SessionManager:
      def __init__(self, cwd: str) -> None:
          self._cwd = cwd
          self._session_id: str | None = None
          self._turn_count: int = 0
          self._lock = threading.Lock()

      def _build_init_prompt(self, user_prompt: str) -> str:
          """Load workspace files and prepend to first message."""
          parts = []
          for filename in ["SOUL.md", "MEMORY.md", "HEARTBEAT.md"]:
              path = os.path.join(self._cwd, "workspace", filename)
              if os.path.exists(path):
                  with open(path) as f:
                      parts.append(f.read().strip())
          parts.append(user_prompt)
          return "\n\n".join(parts)

      def reset(self) -> None:
          with self._lock:
              self._session_id = None
              self._turn_count = 0

      def send(self, prompt: str) -> str | None:
          with self._lock:
              is_new = self._session_id is None
              if is_new:
                  self._session_id = str(uuid.uuid4())
                  self._turn_count = 0

              if self._turn_count >= SESSION_MAX_TURNS:
                  self._session_id = str(uuid.uuid4())
                  self._turn_count = 0
                  is_new = True

              session_id = self._session_id

          if is_new:
              full_prompt = self._build_init_prompt(prompt)
              cmd = ["claude", "-p", full_prompt,
                     "--session-id", session_id,
                     "--permission-mode", "bypassPermissions",
                     "--output-format", "text"]
          else:
              cmd = ["claude", "-p", prompt,
                     "--resume", session_id,
                     "--permission-mode", "bypassPermissions",
                     "--output-format", "text"]

          try:
              result = subprocess.run(
                  cmd, capture_output=True, text=True,
                  timeout=SESSION_TIMEOUT, cwd=self._cwd)
              reply = result.stdout.strip()
              if not reply and result.stderr:
                  if "No conversation found" in result.stderr \
                          or "already in use" in result.stderr:
                      self.reset()
                      if not is_new:
                          return self.send(prompt)
                  return None
              with self._lock:
                  self._turn_count += 1
              return reply
          except subprocess.TimeoutExpired:
              return None
          except Exception:
              return None

Key points:

- Lock around session state. Two concurrent --resume calls to the same
  session will collide. If your bot handles PMs and chat simultaneously,
  the lock prevents race conditions.

- Rotate at ~200 turns. Sessions accumulate context and get sluggish or
  stale. Generate a new UUID, reload workspace files on the first message.

- Detect dead sessions. Check stderr for "No conversation found" or
  "already in use" and reset automatically.

- Build init prompt loads workspace files. Only the first message in a
  session needs the full context. Subsequent messages just send the user
  input.


Output Processing
------------------

Claude Code can emit artifacts that shouldn't reach your users:

- XML-like tool call fragments (<tool_call>, <function_calls>, etc.)
- Markdown formatting (asterisks, backticks)
- JSON tool use blocks

Strip these before sending to your platform. Especially important if your
users rely on screen readers -- markdown formatting is noise.

  import re

  def strip_artifacts(text: str) -> str:
      text = re.sub(r"<tool_call>.*?</tool_call>", "", text, flags=re.DOTALL)
      text = re.sub(r"<tool_result>.*?</tool_result>", "", text, flags=re.DOTALL)
      text = re.sub(r"<function_calls>.*?</function_calls>", "", text, flags=re.DOTALL)
      text = re.sub(r"<.*?>.*?</.*?>", "", text, flags=re.DOTALL)
      text = re.sub(r"\{\"type\":\s*\"tool_use\".*?\}", "", text, flags=re.DOTALL)
      text = text.replace("**", "").replace("*", "").replace("`", "")
      return text.strip()

You may also want to define your own inline markers that the agent can use
and your wrapper strips. Examples:

- [WRITE_MEMORY: ...] -- agent writes to its own memory file
- [FLAG: ...] -- agent flags something for the operator
- [PM:username] -- agent wants to send a private message to a specific user
- SILENT -- agent has nothing to say (don't send an empty message)

These are just conventions. Define whatever your platform needs.


Cron-Based Agents
-----------------

For background tasks -- infrastructure monitoring, memory consolidation,
periodic health checks -- you don't need a persistent bot. You need cron
jobs that invoke claude -p.

The pattern is a shell script that:
1. cd's into the agent's workspace (so CLAUDE.md is loaded)
2. Calls claude -p with a task prompt
3. Pipes output to a notification channel

Example -- infrastructure patrol (runs daily at 6am):

  #!/bin/bash
  set -euo pipefail
  cd /path/to/your/agent/workspace

  RESULT=$(timeout 300 claude -p "Check server health: disk usage (df -h),
  memory/swap (free -h), failed systemd services (systemctl --failed),
  security updates (apt list --upgradable), docker containers (docker ps).
  Write findings to memory. Be brief." \
  --permission-mode bypassPermissions --output-format text 2>&1)

  # Send to your notification channel (Telegram example)
  curl -s -X POST \
      "https://api.telegram.org/bot<TOKEN>/sendMessage" \
      --data-urlencode "chat_id=<CHAT_ID>" \
      --data-urlencode "text=[Infra Patrol] $RESULT" \
      > /dev/null 2>&1 || true

Example -- heartbeat (runs every 2 hours):

  #!/bin/bash
  set -euo pipefail
  cd /path/to/your/agent/workspace

  RESULT=$(timeout 120 claude -p "Read HEARTBEAT.md. Check for unresolved
  tasks, blockers, pending approvals. Current time: $(date '+%H:%M %Z').
  If nothing needs attention, reply exactly: HEARTBEAT_OK" \
  --permission-mode bypassPermissions --output-format text 2>&1)

  if [ "$RESULT" != "HEARTBEAT_OK" ] && [ -n "$RESULT" ]; then
      curl -s -X POST \
          "https://api.telegram.org/bot<TOKEN>/sendMessage" \
          --data-urlencode "chat_id=<CHAT_ID>" \
          --data-urlencode "text=[Heartbeat] $RESULT" \
          > /dev/null 2>&1 || true
  fi

Install via crontab -e:

  0 6 * * *   /path/to/scripts/infra-patrol.sh
  0 */2 * * * /path/to/scripts/heartbeat.sh

Notes:
- Use timeout around claude calls. They can hang.
- Use --data-urlencode for notification API calls, not curl -d. Claude's
  output will contain special characters that break plain -d.
- bypassPermissions is required for the agent to run shell commands and
  access files without interactive confirmation.
- Each cron job is stateless. No session persistence. The agent wakes up,
  does the task, reports, exits.


A2A Communication (Agent-to-Agent)
----------------------------------

This is the honest section. OpenClaw had real A2A infrastructure --
gateway-mediated, multi-agent, proper routing. That's gone.

The practical workaround for most cases is file-based. One agent writes a
file, the other polls for it.

Example: your main agent wants to send a message through your chat bot.
The main agent writes a JSON file:

  echo '[{"to": "username", "text": "message"}]' > /tmp/bot_outbox.json

The chat bot checks that file every second in its main loop:

  def check_outbox(bot) -> None:
      if not os.path.exists("/tmp/bot_outbox.json"):
          return
      with open("/tmp/bot_outbox.json") as f:
          messages = json.load(f)
      os.remove("/tmp/bot_outbox.json")
      for msg in messages:
          bot.send(msg["to"], msg["text"])

This works for low-frequency, one-directional A2A. Which is most of what
people actually need -- a background agent triggering a notification through
a chat bot, or a cron job handing off results.

For bidirectional A2A, you need more. Options:
- Shared directory with inbox/outbox files per agent
- A lightweight HTTP API between agents
- SQLite database as a message queue
- Unix sockets

None of these are as clean as what OpenClaw provided. Pick the simplest
thing that works for your setup.

Passphrase verification still works as a pattern. If your agents need to
trust messages from each other, embed a passphrase:

  Outbound: include "your-secret-phrase" in the message
  Inbound: check for "your-secret-phrase" before acting on instructions

This prevents one agent from being tricked into executing commands from
untrusted sources. Simple, effective, no crypto infrastructure needed.


Sandboxing
----------

If your agent talks to people (chat bots, community bots), sandbox it.
The agent should not know things it shouldn't share.

The practical approach:

1. Separate working directory. The bot runs from /home/you/bots/mybot/,
   not from your main workspace.

2. Dedicated CLAUDE.md. The bot's CLAUDE.md does not reference your private
   infrastructure, credentials, other projects, or personal information.
   It describes the bot's world and nothing else.

3. Restricted file access. The bot's CLAUDE.md should explicitly state what
   files it can access (its own workspace) and what it cannot. Claude
   respects these instructions.

4. No cross-references. Don't have the bot's CLAUDE.md point to your main
   agent's workspace. Don't share memory files. Don't share SOUL.md.

The point is that when someone asks your bot about your server setup, the
correct answer is "I don't know" -- and that should be genuinely true. The
agent shouldn't have the information in the first place.

This is defense in depth. Claude follows instructions well, but the safest
configuration is one where the information simply isn't available.


Gotchas
-------

These are the ones we actually hit. They'll save you time.

1. --allowedDirectory doesn't exist. We had it in our initial code. Claude
   Code doesn't support a directory restriction flag. Use cwd to control
   where Claude runs, and handle path restrictions in your CLAUDE.md.

2. bypassPermissions needed for file I/O. Without it, Claude Code prompts
   for confirmation on every file read/write and shell command. Your
   headless bot can't answer those prompts. Use --permission-mode
   bypassPermissions for any non-interactive invocation.

3. Cold start on first session. The first claude -p call for a new session
   takes several seconds longer than subsequent calls. Users will notice
   the delay on first message. Nothing you can do except set expectations
   or pre-warm sessions.

4. Stateless without --session-id/--resume. Every call without session
   flags is a fresh start. No memory of previous turns. Your bot will
   sound like it has amnesia because it does.

5. Session rotation at ~200 turns. Sessions accumulate context. After
   roughly 200 exchanges, they get sluggish, responses slow down, or the
   agent starts losing coherence. Generate a new UUID and reload workspace
   files.

6. Strip tool artifacts from output. Claude Code can emit XML-like tool
   call fragments in stdout. If you pass these through to your chat
   platform, users see garbage. Strip them.

7. curl -d breaks on special characters. If you're piping Claude's output
   to an API (Telegram, Slack, etc.), use --data-urlencode instead of -d.
   Claude's output will contain quotes, newlines, brackets -- all of which
   break plain form encoding.

8. Home directory CLAUDE.md matters. If your interactive bot runs from
   your home directory (many wrappers do), Claude reads ~/CLAUDE.md. This
   is a different file from your workspace CLAUDE.md. You need the home
   directory file to exist and point to your workspace. Cron scripts that
   cd into the workspace don't have this problem.

9. Subprocess timeout is mandatory. We use 90 seconds for interactive,
   300 for cron jobs. Claude Code can hang. Without a timeout, your bot's
   thread blocks forever and the user gets silence.

10. Threading and concurrent access. If your bot handles multiple messages
    concurrently (PMs and chat at the same time), put a lock around
    session sends. Two concurrent --resume calls to the same session ID
    will collide and one will fail.


Migration Checklist
-------------------

1. Install Claude Code CLI
   curl -fsSL https://claude.ai/install.sh | sh

2. Authenticate
   claude auth login
   Log in with your Max subscription account.

3. Create your agent's directory structure
   mkdir -p /path/to/agent/workspace/memory

4. Write CLAUDE.md
   This is the system prompt. Identity, behavior rules, file access
   boundaries, output format. Keep it self-contained.

5. Write workspace files
   SOUL.md, MEMORY.md, HEARTBEAT.md at minimum. Port identity and memory
   from whatever your agent had before.

6. Build your platform connector
   Replace OpenClaw API calls with subprocess.run(["claude", ...]).
   Implement session management. Add output stripping. Add timeout.

7. Set up cron jobs
   Shell scripts in a scripts/ directory. System crontab entries.
   Notification channel for output.

8. Test in isolation
   Run: claude -p "hello" --output-format text
   from your agent's directory. Confirm it reads your CLAUDE.md and
   responds in character.

9. Back up your OpenClaw config
   cp -r ~/.openclaw/config ~/.openclaw/backup-before-cutover/
   Don't delete it yet.

10. Deploy and monitor
    Watch logs for the first few days. Check session stability, response
    times, memory continuity. Rotate sessions if they get stale.


What's Genuinely Lost
---------------------

This is not a lateral move in every dimension. Be clear about that.

No web UI. You managed agents through a browser before. Now it's CLI,
cron, and log files. If you liked the control panel, it's gone.

A2A is degraded. File-based workarounds work for simple cases. They are
not a replacement for gateway-mediated multi-agent orchestration.

No gateway-level orchestration. OpenClaw could route messages between
agents, manage multiple sessions, handle failover. You build all of that
yourself now, or you don't have it.

Infrastructure is your problem. OpenClaw abstracted away process
management, session persistence, error recovery. Now you write the
systemd units, the cron jobs, the reconnection logic, the error handling.

CLI flags can change without notice. Anthropic hasn't committed to
maintaining --session-id and --resume as stable interfaces. They could
change flags, change behavior, or deprecate features. If they break
--session-id in a future update, the fallback is stateless -p calls with
full context in every prompt. Expensive on tokens, but functional.

The conversation quality is excellent. Claude Code gives you full tool
access, file I/O, shell commands, web search. The model is the same one
you had through OpenClaw. What changed is the plumbing around it. That
plumbing is now your responsibility.


What's Genuinely Better
-----------------------

To be fair about the other side too.

Direct authentication. No third-party token exchange. Your Max
subscription works directly. One less point of failure.

Transparency. You can see exactly what your agent does -- every shell
command, every file read, every tool call. OpenClaw's gateway was a black
box for most operators.

File I/O and tool access. Claude Code gives the agent real file system
access and shell commands. If your agent needs to check git repos, read
logs, run scripts -- it can, natively.

Simpler debugging. Your agent is a subprocess. You can see its stdout,
stderr, exit code. No WebSocket debugging, no gateway logs to parse.

CLAUDE.md as config. Editing a markdown file is simpler than whatever
configuration format OpenClaw used. And the agent reads it as a natural
document, not a parsed config.


Questions
---------

If something in this guide doesn't work, the most likely cause is a
Claude Code version change. Check `claude --version` and compare notes.

If Anthropic breaks --session-id/--resume, the fallback is stateless -p
calls with full context in every prompt. Less efficient, but it works.

This guide is based on what worked as of April 2026. Your mileage may
vary with future Claude Code versions.
