Claude Code Task (Async)
Run Claude Code in background — zero OpenClaw tokens while it works. Results delivered to WhatsApp or Telegram automatically.
Important: Claude Code = General AI Agent
Claude Code is NOT just a coding tool. It's a full-powered AI agent with web search, file access, and deep reasoning. Use it for ANY complex task:
- Research — web search, synthesis, competitive analysis, user experience reports
- Coding — create tools, scripts, APIs, refactor codebases
- Analysis — read and analyze files, data, logs, source code
- Content — write docs, presentations, reports, summaries
- Automations — complex multi-step workflows with file system access
Give it prompts the same way you'd talk to a smart human — natural language, focused on WHAT you need, not HOW to do it.
NOT for:
- Quick questions (just answer directly)
- Tasks needing real-time interaction
Quick Start
Telegram Thread Safety (must-follow)
For Telegram thread runs, run-task.py is designed to either route correctly or fail immediately.
Mandatory step before launch
Resolve the current runtime session key first (source of truth), then launch with it.
- Get current key via
sessions_list(or existing runtime context) - If key is
agent:main:main:thread:<THREAD_ID>→ use it directly in--session - Never derive
--sessionfromchat_id/sender id heuristics
Rules
- Use only
--session "agent:main:main:thread:<THREAD_ID>"for thread tasks - Never use
agent:main:telegram:user:<id>for thread tasks - If routing metadata is inconsistent (thread/session UUID/target mismatch), script exits with
❌ Invalid routing - Default mode is
--telegram-routing-mode auto:- allows non-thread Telegram for setups without thread sessions
- blocks ambiguous user-scope session key (
agent:main:telegram:user:<id>) unless explicitly forced - blocks non-thread launch if a recent thread session exists for same target (likely misroute)
- Force strict thread-only behavior with
--telegram-routing-mode thread-only - Force non-thread behavior with
--telegram-routing-mode allow-non-threador--allow-main-telegram
This is intentional: abort fast > silent misroute.
⚠️ ALWAYS launch via nohup — exec timeout (2 min) will kill the process!
⚠️ NEVER put the task text directly in the shell command — quotes, special characters, and newlines WILL break argument parsing. Always save the prompt to a file first, then use $(cat file).
# Step 1: Save prompt to a temp file
write /tmp/cc-prompt.txt with your task text
# Step 2: Launch with $(cat ...)
nohup python3 {baseDir}/run-task.py \
--task "$(cat /tmp/cc-prompt.txt)" \
--project ~/projects/my-project \
--session "agent:main:whatsapp:group:<JID>" \
--timeout 900 \
> /tmp/cc-run.log 2>&1 &
The --session key (e.g. agent:main:whatsapp:group:120363425246977860@g.us) is used to auto-detect the WhatsApp target.
Telegram (thread-safe default)
# ALWAYS use the current thread session key from context:
# agent:main:main:thread:<THREAD_ID>
nohup python3 {baseDir}/run-task.py \
--task "$(cat /tmp/cc-prompt.txt)" \
--project ~/projects/my-project \
--session "agent:main:main:thread:<THREAD_ID>" \
--timeout 900 \
> /tmp/cc-run.log 2>&1 &
Do NOT use
agent:main:telegram:user:<id>for thread tests/runs. That routes to main chat scope and can drift from the source thread.
Telegram Threaded Mode (1:1 DM with threads)
When Marvin is used in Telegram Threaded Mode, each thread has its own session key like agent:main:main:thread:369520.
Fail-safe routing (NEW): run-task.py now enforces strict thread routing.
- If
--sessioncontains:thread:<id>, the script refuses to start unless Telegram target + thread session UUID are resolved. - It auto-resolves missing values from
sessions_listwhen possible. - If the session is inactive and not returned by API, it falls back to local session files:
~/.openclaw/agents/main/sessions/*-topic-<thread_id>.jsonl. - If provided
--notify-session-idmismatches the session key, it exits with error. - Result: misrouted launches/heartbeats to main chat are blocked before Claude starts.
Use --notify-session-id to wake the exact thread session:
nohup python3 {baseDir}/run-task.py \
--task "$(cat /tmp/cc-prompt.txt)" \
--project ~/projects/my-project \
--session "agent:main:main:thread:369520" \
--timeout 900 \
> /tmp/cc-run.log 2>&1 &
All 5 notification types route to the DM thread when --session key contains :thread:<id> ✅
-
--notify-session-id— optional override. Usually auto-resolved from session metadata/files. -
--notify-thread-id— optional override. Usually auto-extracted from--session. -
--reply-to-message-id— optional debug field; avoid for DM thread routing. -
--validate-only— resolve routing and exit (no Claude run). Use this to verify thread launch args safely. -
--notify-channel— optional override (telegram/whatsapp) -
--notify-target— optional override for chat ID / JID -
--timeout— max runtime in seconds (default: 7200 = 2 hours) -
Always redirect stdout/stderr to a log file
Why file-based prompts?
Research/complex prompts contain single quotes, double quotes, markdown, backticks — any of these break shell argument parsing. Saving to a file and reading with $(cat ...) avoids all quoting issues.
Channel Detection
The detect_channel() function determines where to send notifications:
- CLI override wins — if
--notify-channeland--notify-targetare both provided, those are used exclusively - WhatsApp auto-detect — if the session key contains
@g.us(WhatsApp group JID), WhatsApp is used - No target — if neither applies, notifications are silently skipped
def detect_channel(session_key):
if NOTIFY_CHANNEL_OVERRIDE and NOTIFY_TARGET_OVERRIDE:
return NOTIFY_CHANNEL_OVERRIDE, NOTIFY_TARGET_OVERRIDE
jid = extract_group_jid(session_key)
if jid:
return "whatsapp", jid
return None, None
How It Works
┌─────────────┐ nohup ┌──────────────┐
│ Agent │ ──────────────▶│ run-task.py │
│ (OpenClaw) │ │ (detached) │
└─────────────┘ └──────┬───────┘
│
▼
┌──────────────┐
│ Claude Code │ ← runs on Max subscription ($0 API)
│ (-p mode) │
└──────┬───────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
Every 60s On complete On error/timeout
┌────────┐ ┌──────────┐ ┌──────────────┐
│ ⏳ ping │ │ ✅ result │ │ ❌/⏰/💥 error│
│ silent │ │ channel │ │ channel │
└────────┘ └──────────┘ └──────────────┘
WhatsApp notification flow:
- Heartbeat pings (every 60s) → WhatsApp direct (informational, no agent wake)
- Final result → WhatsApp direct (human sees immediately) +
sessions_send(agent wakes up) - Agent receives
[CLAUDE_CODE_RESULT]via sessions_send → processes it → sends summary viamessage(send)to WhatsApp group - Human sees both: raw result + agent's analysis/next steps
Telegram notification flow (DM Threaded Mode — full pipeline):
- 🚀 Launch notification → thread ✅ (silent; HTML;
<blockquote expandable>for prompt; viasend_telegram_direct) - ⏳ Heartbeat (every 60s) → thread ✅ (silent; plain text; via
send_telegram_direct) - 📡 Claude Code mid-task updates → thread ✅ (on-disk Python script
/tmp/cc-notify-{pid}.py; CC calls file; prefix"📡 🟢 CC: "auto-added) - ✅/❌/⏰/💥 Result notification → thread ✅ (HTML;
<blockquote expandable>for result; viasend_telegram_direct) - 🤖 Agent summary → main chat ⚠️ (known limitation:
openclaw agent --session-idsynthetic messages have nocurrentThreadTs; acceptable)
send_telegram_direct() is the core mechanism for all thread-targeted notifications from external scripts. It calls api.telegram.org directly with message_thread_id — bypasses the OpenClaw message tool entirely (which cannot route to DM threads from outside a session context).
Fallback — if agent wake fails (session locked/busy): already_sent=True is set after the direct send, so no duplicate is sent.
Key detail: Telegram vs WhatsApp delivery
WhatsApp: Raw result sent directly (human sees it immediately) + sessions_send wakes agent for analysis.
Telegram: Result sent via send_telegram_direct → then agent woken via openclaw agent --session-id (no --deliver). The agent sends its response via message(action=send) and replies NO_REPLY. This avoids double messages — --deliver would deliver the agent's turn output on top of any message(action=send) calls inside the turn.
Why not sessions_send for Telegram? sessions_send is blocked in the HTTP /tools/invoke deny list by architectural design. The openclaw agent CLI bypasses this limitation.
Reliability Features
Timeout (default 2 hours)
--timeout 7200→ after 7200s: SIGTERM → wait 10s → SIGKILL- Timeout notification sent to channel with tool call count and last activity
- Partial output saved to file
Crash safety
try/exceptwraps entire main → crash notification always sent- Both channel notification and agent wake attempted on any failure
PID tracking
- PID file written to
skills/claude-code-task/pids/ - Stale PIDs cleaned on startup
- Can check running tasks:
ls skills/claude-code-task/pids/
Silent mode (Telegram only)
Telegram supports silent notifications (no sound). This is used for background/informational messages:
- Heartbeat pings →
silent=True - Launch notifications →
silent=True - Final results →
silent=False(default, user attention needed)
WhatsApp does NOT support silent mode — the flag is ignored for WhatsApp.
Telegram DM Threads vs Forum Groups
Telegram has two distinct thread models. The key difference for run-task.py is how to route messages to the thread.
The core problem with external scripts:
- The OpenClaw
messagetool'sthreadIdparameter is Discord-specific — ignored for Telegram - Target format
"chatId:topic:threadId"is rejected by the message tool's target resolver - Session auto-routing (
currentThreadTs) works ONLY inside active sessions — external scripts have no session context - Solution:
send_telegram_direct()bypasses the message tool entirely; callsapi.telegram.orgdirectly withmessage_thread_id
DM Threaded Mode (bot-user private chat with threads):
- All notifications use
send_telegram_direct(chat_id, text, thread_id=..., parse_mode=...)✅ thread_idauto-extracted from session key*:thread:<id>byextract_thread_id()- Launch + finish:
parse_mode="HTML"with<blockquote expandable>for prompt/result - Heartbeats + mid-task:
parse_mode=None(plain text, avoid Markdown parse errors) parse_mode="Markdown"trap: finish messages contain**text**(CommonMark bold); Telegram MarkdownV1 rejects this with HTTP 400 — messages silently don't arrivereplyTotrap: combiningreplyTo+message_thread_id→ Telegram rejects request → fallback strips thread_id → message lands in main chat- Agent summary:
openclaw agent --session-id <uuid>wakes thread session; response goes to main chat (nocurrentThreadTsin synthetic messages — known, acceptable limitation)
Forum Groups (supergroup with Forum topics enabled):
- Same
send_telegram_direct()approach works;message_thread_idis standard Bot API for Forum topics - Auto-detected from session key pattern
*:thread:<id>
Claude Code mid-task updates:
- DO NOT embed bot tokens or curl commands in the task prompt — Claude Code flags this as prompt injection
- run-task.py writes
/tmp/cc-notify-{pid}.pyto disk before launching Claude Code - Task prompt prepended with
[Automation context: ... python3 /tmp/cc-notify-{pid}.py 'msg' ...] - Claude Code calls the file (legitimate local script pattern, no safety warning)
- Script automatically prepends
"📡 🟢 CC: "to all messages; cleaned up infinallyblock
Notification types
| Event | Emoji | WhatsApp delivery | Telegram delivery | DM thread? | |-------|-------|-------------------|-------------------|------------| | Launch | 🚀 | send_channel (Markdown) | send_telegram_direct (HTML, silent) | ✅ message_thread_id | | Heartbeat | ⏳ | send_channel (Markdown) | send_telegram_direct (plain, silent) | ✅ message_thread_id | | CC mid-task update | 📡 | — | /tmp/cc-notify-{pid}.py (Bot API, silent) | ✅ message_thread_id | | Success | ✅ | send_channel + sessions_send | send_telegram_direct (HTML) + openclaw agent | ✅ message_thread_id | | Error | ❌ | send_channel + sessions_send | send_telegram_direct (HTML) + openclaw agent | ✅ message_thread_id | | Timeout | ⏰ | send_channel + sessions_send | send_telegram_direct (HTML) + openclaw agent | ✅ message_thread_id | | Crash | 💥 | send_channel + sessions_send | send_telegram_direct (HTML) + openclaw agent | ✅ message_thread_id | | Agent summary | 🤖 | — | openclaw agent wake | ⚠️ main chat (no thread ctx) |
Claude Code Flags
-p "task"— print mode (non-interactive, outputs result)--dangerously-skip-permissions— no confirmation prompts--verbose --output-format stream-json— real-time activity tracking for heartbeats
Why NOT exec/pty?
exechas 2 min default timeout → kills long tasks- Even with
pty:true, output has escape codes, hard to parse nohup+-pmode: clean, detached, reliable
Git requirement
Claude Code needs a git repo. run-task.py auto-inits if missing.
Python 3.9 Compatibility
run-task.py uses Optional[X] from typing (not X | None) for compatibility with Python 3.9. The union syntax (X | None) requires Python 3.10+.
# Correct (3.9+)
from typing import Optional
def foo(x: Optional[str]) -> Optional[str]: ...
# Would break on 3.9
def foo(x: str | None) -> str | None: ...
Full E2E Test (reference)
Use this when you need to validate the entire pipeline in one run:
- launch notification in source thread
- heartbeat after >60s
- Claude mid-task progress update (📡 🟢 CC)
- final result in source thread
- agent wake attempt with summary step
Pass criteria
- Launch message appears in the same thread (with expandable prompt quote)
- At least one wrapper heartbeat appears after ~60s
- At least one mid-task CC update appears (via
/tmp/cc-notify-<pid>.py) - Final result appears in the same thread (expandable result quote)
- Agent wake is attempted (
openclaw agent --session-id ...) and does not duplicate final result
Canonical full test prompt pattern
- keep prompt compact (about 10 lines) for routine testing
- ensure prompt length is >4500 chars to verify quote truncation/collapse behavior in Telegram
- force runtime >60s (
sleep 70) to trigger wrapper heartbeat - explicitly instruct Claude to call the notify script at least twice
- include a short structured report so output is easy to verify
Canonical launch (minimal mode)
cat > /tmp/cc-full-test-prompt.txt << 'EOF'
# ~10 lines, but total >4500 chars:
# 1) notify script now
# 2) create test file with repeated text (to exceed 4500 chars)
# 3) sleep 70 + notify script again
# 4) run several shell commands
# 5) return short structured report
EOF
python3 {baseDir}/run-task.py \
--task "$(cat /tmp/cc-full-test-prompt.txt)" \
--project /tmp/cc-e2e-project \
--session "agent:main:main:thread:<THREAD_ID>" \
--validate-only
nohup python3 {baseDir}/run-task.py \
--task "$(cat /tmp/cc-full-test-prompt.txt)" \
--project /tmp/cc-e2e-project \
--session "agent:main:main:thread:<THREAD_ID>" \
--timeout 900 \
> /tmp/cc-full-test.log 2>&1 &
Verification artifacts
- wrapper log:
/tmp/cc-full-test.log - Claude output:
/tmp/cc-YYYYMMDD-HHMMSS.txt - session registry entry in
~/.openclaw/claude_sessions.json
Examples
WhatsApp: Create a tool
nohup python3 {baseDir}/run-task.py \
-t "Create a Python CLI tool that converts markdown to HTML with syntax highlighting. Save as convert.py" \
-p ~/projects/md-converter \
-s "agent:main:whatsapp:group:120363425246977860@g.us" \
> /tmp/cc-run.log 2>&1 &
Telegram: Research codebase (thread-safe)
nohup python3 {baseDir}/run-task.py \
--task "$(cat /tmp/cc-prompt.txt)" \
--project ~/projects/my-project \
--session "agent:main:main:thread:<THREAD_ID>" \
--timeout 1800 \
> /tmp/cc-run.log 2>&1 &
Telegram Threaded Mode: Research codebase
nohup python3 {baseDir}/run-task.py \
--task "$(cat /tmp/cc-prompt.txt)" \
--project ~/projects/my-project \
--session "agent:main:main:thread:369520" \
--timeout 1800 \
> /tmp/cc-run.log 2>&1 &
# thread_id auto-extracted from session key
# target + session UUID auto-resolved from API/local session files
Telegram Threaded Mode: Mid-task updates from Claude Code
run-task.py automatically creates an on-disk notification script before launching Claude Code, so CC can send progress updates without seeing the bot token in the prompt (which triggers safety refusals):
# Just write a normal task prompt — run-task.py handles the rest
cat > /tmp/cc-prompt.txt << 'EOF'
STEP 1: Write analysis to /tmp/report.txt (600+ words)...
After step 1, send a progress notification using the script from the
automation context above: python3 /tmp/cc-notify-<PID>.py "Step 1 done."
STEP 2: Write summary to /tmp/summary.txt...
EOF
nohup python3 {baseDir}/run-task.py \
--task "$(cat /tmp/cc-prompt.txt)" \
--project ~/projects/my-project \
--session "agent:main:main:thread:<THREAD_ID>" \
--timeout 1800 \
> /tmp/cc-run.log 2>&1 &
# run-task.py writes /tmp/cc-notify-{pid}.py before launch
# Prepends "[Automation context: use python3 /tmp/cc-notify-{pid}.py 'msg']" to task
# Claude Code calls the file; prefix "📡 🟢 CC: " auto-added; file cleaned up on exit
⚠️ Never embed bot tokens or curl commands in the task prompt — Claude Code correctly identifies hardcoded tokens + external API calls as prompt injection and refuses. Use the on-disk script pattern above instead.
Quick reference: launching from a Telegram DM thread (minimal mode)
# 1) Validate routing first (no Claude run) python3 {baseDir}/run-task.py \ --task "probe" \ --project ~/projects/x \ --session "agent:main:main:thread:<THREAD_ID>" \ --validate-only # 2) Real launch (only 3 required params) nohup python3 {baseDir}/run-task.py \ --task "$(cat /tmp/prompt.txt)" \ --project ~/projects/x \ --session "agent:main:main:thread:<THREAD_ID>" \ --timeout 900 \ > /tmp/cc-run.log 2>&1 &
- Required:
--task,--project,--session
- Safety: Telegram launches without
:thread:<id>are blocked by default (❌ Unsafe routing blocked) - For non-thread Telegram deployments, use
--telegram-routing-mode allow-non-thread.
THREAD_IDis auto-extracted from session key- Target + session UUID are auto-resolved (API, then local session-file fallback)
- If routing is inconsistent/unresolved, script exits with
❌ Invalid routingbefore run- All notifications from run-task (launch/heartbeat/result) stay on the source thread ✅
Long task with extended timeout
nohup python3 {baseDir}/run-task.py \
-t "Refactor the entire auth module to use JWT tokens" \
-p ~/projects/backend \
-s "agent:main:whatsapp:group:120363425246977860@g.us" \
--timeout 3600 \
> /tmp/cc-run.log 2>&1 &
Cost
- Claude Code runs on Max subscription ($200/mo) — NOT API tokens
- Zero OpenClaw API cost while Claude Code works
- Only cost: message delivery + brief agent turn for summary
Session Resumption
Claude Code sessions can be resumed to continue previous conversations. This is useful for:
- Follow-up tasks building on previous research
- Continuing after timeouts or interruptions
- Multi-step workflows where context matters
How to Resume
When a task completes, the session ID is automatically captured and saved to the registry (~/.openclaw/claude_sessions.json).
To resume a session, use the --resume flag:
nohup python3 {baseDir}/run-task.py \
--task "$(cat /tmp/cc-prompt.txt)" \
--project ~/projects/my-project \
--session "SESSION_KEY" \
--resume <session-id> \
> /tmp/cc-run.log 2>&1 &
Session Labels
Use --session-label to give sessions human-readable names for easier tracking:
nohup python3 {baseDir}/run-task.py \
--task "$(cat /tmp/cc-prompt.txt)" \
--project ~/projects/my-project \
--session "SESSION_KEY" \
--session-label "Research on Jackson Berler" \
> /tmp/cc-run.log 2>&1 &
Listing Recent Sessions
The agent can read the session registry to find recent sessions:
# Python code (for agent automation)
from session_registry import list_recent_sessions, find_session_by_label
# List sessions from last 72 hours
recent = list_recent_sessions(hours=72)
for session in recent:
print(f"{session['session_id']}: {session['label']} ({session['status']})")
# Find session by label (fuzzy match)
session = find_session_by_label("Jackson")
if session:
print(f"Found: {session['session_id']}")
Or manually inspect the registry:
cat ~/.openclaw/claude_sessions.json
When to Resume vs Start Fresh
Resume when:
- You need context from previous conversation
- Building on previous research/analysis
- Continuing interrupted work
- Following up with clarifications or next steps
Start fresh when:
- Completely unrelated task
- Previous session was exploratory/experimental
- You want a clean slate
- Previous session context might cause confusion
Resume Failure Handling
If a session ID is invalid or expired:
- Error message sent to channel with suggestion to start fresh
- Process exits cleanly (no partial work)
- Check stderr in
/tmp/cc-run.logfor details
Common resume failures:
- Session expired (Claude Code has retention limits)
- Invalid session ID (typo, wrong format)
- Session from different project/context
Example Workflow
Step 1: Initial research
# Save prompt
write /tmp/research-prompt.txt with "Research the codebase architecture for project X"
# Launch task (Telegram thread-safe example)
nohup python3 {baseDir}/run-task.py \
--task "$(cat /tmp/research-prompt.txt)" \
--project ~/projects/project-x \
--session "agent:main:main:thread:<THREAD_ID>" \
--session-label "Project X architecture research" \
> /tmp/cc-run.log 2>&1 &
Step 2: Check result and find session ID
# Session ID printed in stderr: "📝 Session registered: <id>"
tail /tmp/cc-run.log
# Or read from registry
cat ~/.openclaw/claude_sessions.json | grep "Project X"
Step 3: Follow-up implementation
# Save follow-up prompt
write /tmp/implement-prompt.txt with "Based on your research, implement the authentication module"
# Resume session
nohup python3 {baseDir}/run-task.py \
--task "$(cat /tmp/implement-prompt.txt)" \
--project ~/projects/project-x \
--session "SESSION_KEY" \
--resume <session-id-from-step-1> \
--session-label "Project X auth implementation" \
> /tmp/cc-run2.log 2>&1 &
Files
skills/claude-code-task/
├── SKILL.md # This file
├── run-task.py # Async runner with notifications
├── session_registry.py # Session metadata storage
└── pids/ # PID files for running tasks (auto-managed)
Scan to join WeChat group