Install
openclaw skills install codex-cli-taskLaunch OpenAI Codex CLI async in background with automatic delivery to Telegram/WhatsApp. Use for coding, refactoring, codebase research, file generation, an...
openclaw skills install codex-cli-taskRun OpenAI Codex CLI in background — zero OpenClaw tokens while it works. Results delivered to WhatsApp or Telegram automatically.
Codex is NOT just a coding tool. In codex exec mode it is a general-purpose AI agent with file access, shell execution, optional web search, and deep reasoning.
Use it for:
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:
When user asks things like:
it means run the full E2E operator validation flow for run-task.py routing + notifications.
It does NOT mean plain pytest/unittest discovery by default.
Required behavior:
--validate-only).nohup and file-based prompt.Use the canonical protocol: references/testing-protocol.md and the section below Full E2E Test (reference).
run-task.py is asynchronous orchestration.
After a successful nohup launch, the correct behavior is:
Do not keep waiting in the same turn for Codex completion. Do not poll and then summarize in the same turn unless user explicitly asked for active live monitoring.
Anti-pattern:
run-task.py and keep responding as if completion should appear in this turnCorrect pattern:
run-task.py → acknowledge launch → stop → wait for wakeNever claim "launched" until you have positive launch proof.
Required proof checklist:
nohup command returned a PIDps -p <PID>)🔧 Starting OpenAI Codex... or equivalent startup marker--validate-only) for Telegram thread runsIf launch fails with ❌ Invalid routing:
sessions_listBefore launching Codex, post a short plan in chat:
If staged: explicitly say this run is "phase 1" and what signal decides phase 2.
For Telegram thread runs, run-task.py is designed to either route correctly or fail immediately.
Resolve the current runtime session key first, then launch with it.
sessions_list or runtime contextagent:main:main:<thread|topic>:<THREAD_ID> → use it directly in --session--session from chat_id / sender heuristics--session "agent:main:main:<thread|topic>:<THREAD_ID>" for thread tasksagent:main:telegram:user:<id> for thread tasks❌ Invalid routing--telegram-routing-mode auto--telegram-routing-mode thread-only--telegram-routing-mode allow-non-thread or --allow-main-telegramThis is intentional: abort fast > silent misroute
⚠️ ALWAYS launch via nohup — exec timeout will kill the process otherwise.
⚠️ NEVER put the task text directly in the shell command — save the prompt to a file first, then use $(cat file).
# Step 1: Save prompt to a temp file
write /tmp/codex-prompt.txt with your task text
# Step 2: Launch with $(cat ...)
nohup python3 {baseDir}/run-task.py \
--task "$(cat /tmp/codex-prompt.txt)" \
--project ~/projects/my-project \
--session "agent:main:whatsapp:group:<JID>" \
--timeout 900 \
> /tmp/codex-run.log 2>&1 &
nohup python3 {baseDir}/run-task.py \
--task "$(cat /tmp/codex-prompt.txt)" \
--project ~/projects/my-project \
--session "agent:main:main:<thread|topic>:<THREAD_ID>" \
--timeout 900 \
> /tmp/codex-run.log 2>&1 &
Do NOT use
agent:main:telegram:user:<id>for thread tests/runs.
When OpenClaw is used in Telegram threaded mode, each thread has its own session key like agent:main:main:thread:369520 or agent:main:main:topic:369520.
Fail-safe routing (NEW): run-task.py now enforces strict thread routing.
--session contains :thread:<id> or :topic:<id>, the script refuses to start unless Telegram target + thread session UUID are resolved.sessions_list when possible.~/.openclaw/agents/main/sessions/*-topic-<thread_id>.jsonl.--notify-session-id mismatches the session key, it exits with error.Use --notify-session-id to wake the exact thread session:
nohup python3 {baseDir}/run-task.py \
--task "$(cat /tmp/codex-prompt.txt)" \
--project ~/projects/my-project \
--session "agent:main:main:<thread|topic>:369520" \
--timeout 900 \
> /tmp/codex-run.log 2>&1 &
All 5 notification types route to the DM thread when --session key contains :thread:<id> or :topic:<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 Codex run). Use this to verify thread launch args safely.--notify-channel — optional channel hint (telegram/whatsapp); target is always auto-resolved from session metadata.--timeout — max runtime in seconds (default: 7200 = 2 hours)--completion-mode — optional legacy hint (single default, iterate if explicitly needed)--max-iterations — optional budget hint when using iterate mode--trace-live — emit live technical trace markers into the same chat/thread (debug mode)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.
The detect_channel() function determines where to send notifications:
@g.us (WhatsApp group JID), WhatsApp is used❌ Invalid routing instead of silent misroutedef 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
┌─────────────┐ nohup ┌──────────────┐
│ Agent │ ──────────────▶│ run-task.py │
│ (OpenClaw) │ │ (detached) │
└─────────────┘ └──────┬───────┘
│
▼
┌──────────────┐
│ Codex │
│ codex exec │
└──────┬───────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
Every 60s On complete On error/timeout
┌────────┐ ┌──────────┐ ┌──────────────┐
│ ⏳ ping │ │ ✅ result │ │ ❌/⏰/💥 error│
│ silent │ │ channel │ │ channel │
└────────┘ └──────────┘ └──────────────┘
sessions_send--completion-mode is optional and acts as a hint:
single = one run → continuation summary → stopiterate = continuation summary + exactly one next iteration when gaps remainWake payload frames continuation as the same ongoing OpenClaw conversation after Codex replies to the previous launch.
In iterate mode:
run_id and wake_idrun-task.py keeps per-project state in /tmp/codex-orchestrator-state-<hash>.json[TRACE][AGENT][WAKE_RECEIVED] ...[TRACE][AGENT][DECISION] continue|stop ...<blockquote expandable> for prompt; via send_telegram_direct; includes Resume: <thread-id|new>)send_telegram_direct)/tmp/codex-notify-{pid}.py; Codex calls file; prefix "📡 🟢 Codex: " auto-added)<blockquote expandable> for result; via send_telegram_direct)openclaw agent --deliver ✅ (same session continuation is visible to user)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.
WhatsApp: Raw result sent directly (human sees it immediately) + sessions_send wakes agent for analysis.
Telegram: Result sent via send_telegram_direct → then agent is woken via openclaw agent --session-id --deliver so the continuation turn is visible in chat by default. This is the intended “same agent, same conversation” behavior after Codex completion.
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.
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:
message tool's threadId parameter is Discord-specific — ignored for Telegram"chatId:topic:threadId" is rejected by the message tool's target resolversend_telegram_direct() bypasses the message tool entirely; calls api.telegram.org directly with message_thread_idDM Threaded Mode (bot-user private chat with threads):
send_telegram_direct(chat_id, text, thread_id=..., parse_mode=...) ✅thread_id auto-extracted from session key *:thread:<id> or *:topic:<id> by extract_thread_id()parse_mode="HTML" with <blockquote expandable> for prompt/resultparse_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 arrivereplyTo trap: combining replyTo + message_thread_id can cause Telegram to reject the request or route incorrectlyopenclaw agent --session-id <uuid> --deliver publishes the wake turn to chat so the user sees the same ongoing assistant conversationForum Groups (supergroup with Forum topics enabled):
send_telegram_direct() approach works; message_thread_id is standard Bot API for Forum topics*:thread:<id> or *:topic:<id>Codex mid-task updates:
run-task.py writes /tmp/codex-notify-{pid}.py to disk before launching Codex[Automation context: ... python3 /tmp/codex-notify-{pid}.py 'msg' ...]"📡 🟢 Codex: " to all messages; cleaned up in finally block--timeout 7200 → after 7200s: SIGTERM → wait 10s → SIGKILLtry/except wraps entire main → crash notification always sentskills/codex-cli-task/pids/ls skills/codex-cli-task/pids/Telegram supports silent notifications (no sound).
Current policy: all Codex notifications are silent in Telegram:
silent=Truesilent=True📡 🟢 Codex) → silent=Truesilent=Truesilent=TrueWhatsApp does NOT support silent mode — the flag is ignored for WhatsApp.
| 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 |
| Codex mid-task update | 📡 | — | /tmp/codex-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 continuation reply | 🤖 | — | openclaw agent wake (--deliver) | ✅ visible in chat |
exec "task" — non-interactive runresume <thread-id> "task" — continue a previous Codex session--dangerously-bypass-approvals-and-sandbox — no confirmation prompts--json --output-last-message — real-time activity tracking + final output capture--full-auto — optional safer automation modeexec has 2 min default timeout → kills long taskspty:true, output has escape codes, hard to parsenohup + detached runner: clean, detached, reliableCodex needs a git repo. run-task.py auto-inits if missing.
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: ...
Use this when you need to validate the entire pipeline in one run:
/tmp/codex-notify-<pid>.py)openclaw agent --session-id ... --deliver) and appears visibly in chatsleep 70) to trigger wrapper heartbeatFor iterate-mode testing, do exactly one continuation step after phase 1.
Reason: validates the iterative path without turning a routine test into a long multi-hop run.
Between ✅ OpenAI Codex completed and any next 🚀 OpenAI Codex started, there must be a user-facing analysis message in the thread.
cat > /tmp/codex-full-test-prompt.txt << 'EOF'
# 1) notify script now
# 2) create test file
# 3) sleep 70 + notify again
# 4) run several shell commands
# 5) return short structured report
EOF
python3 {baseDir}/run-task.py \
--task "$(cat /tmp/codex-full-test-prompt.txt)" \
--project /tmp/codex-e2e-project \
--session "agent:main:main:<thread|topic>:<THREAD_ID>" \
--validate-only
nohup python3 {baseDir}/run-task.py \
--task "$(cat /tmp/codex-full-test-prompt.txt)" \
--project /tmp/codex-e2e-project \
--session "agent:main:main:<thread|topic>:<THREAD_ID>" \
--timeout 900 \
> /tmp/codex-full-test.log 2>&1 &
/tmp/codex-full-test.log/tmp/codex-YYYYMMDD-HHMMSS.txt~/.openclaw/codex_sessions.jsonIf a Codex task is expected to run longer than ~1 minute, explicitly ask Codex to send intermediate progress updates during execution.
Recommended wording:
For Telegram thread-safe runs, updates should use the injected automation script (/tmp/codex-notify-<pid>.py).
cat > /tmp/codex-full-test-prompt.txt << 'EOF'
# ~10 lines
# 1) use notify helper now
# 2) create a test artifact
# 3) sleep 70 + notify again
# 4) run several shell commands
# 5) return short structured report
EOF
python3 {baseDir}/run-task.py \
--task "$(cat /tmp/codex-full-test-prompt.txt)" \
--project /tmp/codex-e2e-project \
--session "agent:main:main:<thread|topic>:<THREAD_ID>" \
--validate-only
nohup python3 {baseDir}/run-task.py \
--task "$(cat /tmp/codex-full-test-prompt.txt)" \
--project /tmp/codex-e2e-project \
--session "agent:main:main:<thread|topic>:<THREAD_ID>" \
--timeout 900 \
> /tmp/codex-full-test.log 2>&1 &
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/codex-run.log 2>&1 &
nohup python3 {baseDir}/run-task.py \
--task "$(cat /tmp/codex-prompt.txt)" \
--project ~/projects/my-project \
--session "agent:main:main:<thread|topic>:<THREAD_ID>" \
--timeout 1800 \
> /tmp/codex-run.log 2>&1 &
run-task.py automatically creates an on-disk notification script before launching Codex, so Codex can send progress updates without seeing bot tokens in the prompt.
cat > /tmp/codex-prompt.txt << 'EOF'
STEP 1: Write analysis to /tmp/report.txt.
After step 1, send a progress notification using the script from the
automation context above.
STEP 2: Write summary to /tmp/summary.txt.
EOF
nohup python3 {baseDir}/run-task.py \
--task "$(cat /tmp/codex-prompt.txt)" \
--project ~/projects/my-project \
--session "agent:main:main:<thread|topic>:<THREAD_ID>" \
--timeout 1800 \
> /tmp/codex-run.log 2>&1 &
Never embed bot tokens or raw curl commands in the task prompt.
Quick reference: launching from a Telegram DM thread (minimal mode)
python3 {baseDir}/run-task.py \ --task "probe" \ --project ~/projects/x \ --session "agent:main:main:<thread|topic>:<THREAD_ID>" \ --validate-only nohup python3 {baseDir}/run-task.py \ --task "$(cat /tmp/prompt.txt)" \ --project ~/projects/x \ --session "agent:main:main:<thread|topic>:<THREAD_ID>" \ --timeout 900 \ > /tmp/codex-run.log 2>&1 &
- Required:
--task,--project,--sessionTHREAD_IDis auto-extracted from session key- target + session UUID are auto-resolved when possible
- if routing is inconsistent/unresolved, script exits with
❌ Invalid routing- launch/heartbeat/result notifications stay on the source thread
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/codex-run.log 2>&1 &
Codex sessions can be resumed to continue previous conversations. This is useful for:
--resume takes the Codex thread_id, not run_id or wake_id.
Correct source:
📝 Session registered: <thread-id-here>
That is the value to pass as --resume <thread-id>.
When a task completes, the Codex thread_id is captured and saved to ~/.openclaw/codex_sessions.json.
nohup python3 {baseDir}/run-task.py \
--task "$(cat /tmp/codex-prompt.txt)" \
--project ~/projects/my-project \
--session "SESSION_KEY" \
--resume <thread-id> \
> /tmp/codex-run.log 2>&1 &
Use --session-label to give sessions human-readable names for easier tracking.
from session_registry import list_recent_sessions, find_session_by_label
recent = list_recent_sessions(hours=72)
for session in recent:
print(f"{session['thread_id']}: {session['label']} ({session['status']})")
Or manually inspect:
cat ~/.openclaw/codex_sessions.json
Resume when:
Start fresh when:
If a thread_id is invalid or expired:
/tmp/codex-run.logCommon resume failures:
Step 1: Initial research
write /tmp/research-prompt.txt with "Research the codebase architecture for project X"
nohup python3 {baseDir}/run-task.py \
--task "$(cat /tmp/research-prompt.txt)" \
--project ~/projects/project-x \
--session "agent:main:main:<thread|topic>:<THREAD_ID>" \
--session-label "Project X architecture research" \
> /tmp/codex-run.log 2>&1 &
Step 2: Find thread_id
tail /tmp/codex-run.log
cat ~/.openclaw/codex_sessions.json | grep "Project X"
Step 3: Follow-up implementation
write /tmp/implement-prompt.txt with "Based on your research, implement the authentication module"
nohup python3 {baseDir}/run-task.py \
--task "$(cat /tmp/implement-prompt.txt)" \
--project ~/projects/project-x \
--session "SESSION_KEY" \
--resume <thread-id-from-step-1> \
--session-label "Project X auth implementation" \
> /tmp/codex-run2.log 2>&1 &
When the agent wake / continue chain fails:
sessions_send is enabledall--validate-only/tmp/codex-run.logCommon failure patterns:
This is the current intended behavior of the Codex adaptation:
thread_id values captured from the JSON event streamCurrent locally validated behavior in this repo:
exec via --jsonresume--output-last-message