# Trevor's OpenClaw Setup — Technical Gotchas & Learned Patterns

This file documents bugs, workarounds, and hard-won patterns specific to this OpenClaw deployment.
Personal credentials are NOT included — they live in `.env.*` files locally.

---

## Environment
- **Host:** macOS Darwin 25.2.0 (arm64, Apple Silicon)
- **Workspace:** `/Users/trevorpope/clawd`
- **State dir:** `~/.openclaw/`
- **Config:** `~/.openclaw/clawdbot.json`
- **Temp writes:** `~/clawd/tmp/` (NOT `/tmp/` — isolated sessions can't reach `/tmp/`)
- **Primary channel:** Telegram
- **Telegram target (Trevor):** numeric ID `54843073` (username `trevorpope` fails with 400)
- **Primary model:** `anthropic/claude-sonnet-4-6` (main session)
- **Cron/subagent model:** `openrouter/openai/gpt-5.5` or `google/gemma-4-31b-it`

---

## CRON SYSTEM GOTCHAS

### sessionTarget rules (CRITICAL — many silent failures here)
- `sessionTarget: "main"` REQUIRES `payload.kind: "systemEvent"`. Using `agentTurn` + main silently skips the job without error.
- `sessionTarget: "isolated"` REQUIRES `payload.kind: "agentTurn"`

### Isolated cron sessions have NO Telegram bot token
- The `message` tool fails in isolated crons with "Telegram bot token missing"
- Solution: output alert text as the agent reply (delivery.mode=announce), OR use CLI: `openclaw message send --channel telegram --target 54843073 --message "..."`
- Last resort: read bot token via exec cat + raw curl to Telegram API

### `openclaw cron edit` CLI flags — what does NOT exist
- NO `--delivery` flag → use `--channel <channel>` and `--announce` separately
- NO `--tools-allow` flag → use `--clear-tools` to remove allowlist entirely
- NO `--session-target` flag → use `--session <main|isolated>`
- NO `--schedule` flag → use `--cron "<expr>"`, `--every <duration>`, or `--at <when>`

### `delivery.accountId` null-patch silently ignored
- Patching `delivery.accountId` to `null` via the cron API is silently ignored — in-memory value stays
- Workaround: patch `delivery.mode="none"` first via API, then edit jobs.json after (API always overwrites file)

### jobs.json edit sequence (critical)
- The API always persists in-memory state to disk when you call `cron update`
- Sequence MUST be: API update first → then file edit for changes API can't express
- `jobs.json` is atomically written via tmp file swap — never edit directly while gateway is running

### Cron 600s hard cap
- Outer execution wrapper enforces 600s (10 min) ceiling regardless of `timeoutSeconds`
- Fix: reduce script processing volume, not the timeout config

### `openclaw cron list --json` quirks
- NOT a bare array — parse top-level keys: `data.jobs` or similar
- May include leading warnings/extra output — use `JSONDecoder.raw_decode()` not plain `json.loads()`
- Topic search via `grep` unreliable (names truncated in display) — search `.prompt` field in JSON

### Cron dedup MUST use local timezone
- Dedup keys computed with `toISOString().slice(0,10)` use UTC → roll at 5 PM PDT
- Use: `now.toLocaleDateString('en-CA', { timeZone: 'America/Los_Angeles' })` → YYYY-MM-DD in PT

### Gateway restarts = at-least-once cron delivery
- macOS memory pressure restarts gateway ~8x/day
- Each restart can re-trigger "overdue" crons
- Every Telegram/email-sending cron must have an in-script idempotent dedup gate

### cron-list-safe.sh false positive (active bug)
- `~/clawd/scripts/cron-list-safe.sh` reports clean even when jobs have `lastRunStatus: error`
- Root cause: jq query `select(.state == "error")` fails because `state` is a nested object
- DO NOT trust this script for error detection — use `openclaw cron list --json` + Python parsing

### `openclaw cron wake` has no `--text` flag
- `openclaw cron wake --text "..."` returns "does not recognize option"
- Use the `cron` tool from main session: `cron(action=wake, text="...")`

### Dispatch stall — does NOT self-recover
- "Isolated agent run stalled before execution start (last phase: attempt-dispatch)" errors
- These do NOT reliably self-recover (observed 8 consecutive days)
- Fix: inspect and fix the job immediately, don't wait

### Model issues in isolated crons
- `openai/gpt-5.5` direct API fails if key expires → use `openrouter/openai/gpt-5.5`
- `gpt-5.5` in isolated crons unreliably respects "don't call message tool" constraint
- TPM rate limits spike 5 PM–midnight PT → use `gpt-4.1-mini` or `claude-haiku-4-5` for script-runner crons
- `gpt-4o` is confirmed exec-capable in isolated crons

### Isolated cron runs under ~15 seconds = exec didn't actually run
- Any isolated agentTurn cron completing in ≤15s likely ran `NO_REPLY` without executing anything
- Detection: check `openclaw cron runs --id <jobId>` duration
- Fix: switch model to exec-capable one, add explicit exec verification step in prompt

### Cron exit-code suppression
- Inline bash-c tricks (`|| true`, redirect to /dev/null) unreliable when `gpt-4o`/`gpt-5.5` is runner model
- Reliable fix: dedicated `.sh` wrapper that runs real command and unconditionally `exit 0`

### Shell scripts with curl
- Always include `--max-time 120` on any curl in a cron-called script
- Unbounded curl can hang until the 600s hard cap

### cron "messages never arrive" — check tools allowlist FIRST
- If `message` tool not in the cron's tools allowlist, tool calls silently no-op
- Check the allowlist before debugging message logic

---

## GATEWAY & CONFIG GOTCHAS

### Gateway restart = Telegram outage
- Restart = ~30-60 second Telegram outage — only restart when actually needed
- Exception: gateway dead, then restart immediately

### `commands.restart=false` fallback
- If `gateway restart` tool returns `commands.restart=false`, use launchctl:
  `launchctl stop ai.openclaw.gateway && launchctl start ai.openclaw.gateway`

### Post-update version staleness
- Session context header shows old version for one restart after update
- ALWAYS verify via `openclaw --version` CLI, not the session banner

### config.patch size-check (MANDATORY)
- Verify config byte count did NOT drop >50% after any patch
- Before risky changes: `openclaw backup create --verify`

### `openclaw devices list` can restart gateway
- Use `nodes device_status` instead for checking device state

### Invalid config behavior
- Invalid external config edits: gateway rejects and creates `.rejected.*` file
- `openclaw doctor --fix` can repair clobbered/corrupted config
- `.clobbered.*` = rejected payload preserved for inspection
- Latest 32 `.clobbered.*` files kept per config path

### Multiple gateway services = bad
- `openclaw gateway status --deep` to scan for duplicates
- `openclaw doctor --fix` removes stale/duplicate service registrations

### macOS maintenance sleep crashes (pre-2026.5.26)
- Power Nap puts Wi-Fi driver into state 0, causing ENETDOWN → gateway crash
- Gateway can stay down for HOURS after launchd respawn gate parked it
- Fix: upgrade past 2026.5.26 AND `sudo pmset -a sleep 0 disksleep 0 standby 0 powernap 0`
- Watchdog: `launchctl kickstart -k gui/$UID/ai.openclaw.gateway` if state != running

---

## SESSIONS & MODELS GOTCHAS

### Session poison: "Invalid signature in thinking block"
- Symptom: every heartbeat returns `[assistant turn failed before producing content]`
- Root cause: cached thinking block with stale/malformed signature poisons context
- Recovery: start fresh session — it CANNOT self-heal
- Prevention: avoid long-lived cached contexts with thinking enabled across gateway restarts

### `openclaw sessions` CLI — no subcommands
- Only accepts flags (`--active`, `--json`, `--help`)
- `sessions log <name>` or `sessions history <key>` = INVALID, fail with "too many arguments"
- To get exec stdout: use `process` tool (`action=poll` or `action=log`)
- To get conversation history: use `sessions_history` tool

### `openclaw sessions --json` — noisy stdout
- Config warnings leak into stdout before the JSON
- Always pipe stderr separately before parsing: `openclaw sessions --json 2>/dev/null`

### claude-opus-4-7 thinking.type bug
- Returns `400: thinking.type.enabled is not supported` on first turn of every run
- Auto-retries successfully; subsequent turns clean
- One wasted round-trip (~2-5k tokens duplicated) per run — known upstream bug

### Model routing (this setup)
- Main session: `anthropic/claude-sonnet-4-6`
- Complex crons: `openrouter/anthropic/claude-sonnet-4`
- Simple crons: `openrouter/openai/gpt-5.5` or `google/gemma-4-31b-it`
- DEAD models (do not use): `hunter-alpha`, `healer-alpha`, `haiku`, `gpt-5.4-pro`, `mimo-v2-pro`

---

## EXEC TOOL GOTCHAS

### Sandbox boundary
- `read` tool fails with "Path escapes sandbox root" for files outside `~/clawd`
- Skill files at `~/.openclaw/skills/`: use `exec cat` not `read` tool
- Inbound media at `~/.openclaw/media/inbound/`: copy to `~/clawd/tmp/` first

### Path expansion
- `bash ~/path/to/script.sh` — `~` not always expanded; use `$HOME/path` or absolute
- `cd <dir> && python3 scripts/file.py` triggers "complex interpreter invocation" block → use absolute path

### zsh readonly variable `status`
- `status=$?` in zsh scripts fails with "read-only variable: status"
- Use `rc=$?` or `exit_code=$?` instead

### `/tmp` writes
- Isolated sessions can't reach `/tmp` — always use `~/clawd/tmp/` for all cron writes

### exec JSON data with apostrophes
- Shell argument quoting breaks with apostrophes in JSON values
- Fix: write JSON to temp file, have script read it

---

## BROWSER TOOL GOTCHAS

### Tab management
- `browser(action=navigate)` reuses active tab
- `browser(action=open)` spawns fresh tab — use this when you want a new tab
- Always capture `targetId` after `open` and pass to all subsequent actions

### Profile quirks
- `profile="user"`: no `timeoutMs` override on `type`, `evaluate`, `fill`, `hover`, `scrollIntoView`, `drag`, `select`
- `profile="openclaw"`: can fail with "Chrome CDP websocket not reachable" → check `browser status` first
- Before declaring browser dead: probe `http://127.0.0.1:18800/json`

### This gateway build limitations
- `act:evaluate` and `act:type` = UNSUPPORTED ("Playwright is not available in this gateway build")
- `act:click` and `snapshot` still work
- For JS form verification: use `curl` + direct endpoint POST tests

### `act:fill` requires fields array
- Correct: `{kind:"fill", fields:[{ref:"e33",value:"username"}]}`
- NOT bare `value` parameter

### `act` targetId consistency
- `browser(action=act)` targetId must match nested `request.targetId` — or omit nested one

### `web_fetch` on Cloudflare-protected sites
- Returns 403 "Just a moment..." — Cloudflare blocks non-browser fetches
- Use `browser(action=open)` + `snapshot` instead

### Browser snapshots
- `mode: efficient` + `labels: true` requires `snapshotFormat: ai` — `aria` with these options errors
- When `browser navigate` times out: try `snapshot` on the same tab before restarting anything

---

## TELEGRAM / MESSAGING GOTCHAS

### Telegram target ID
- Trevor's numeric ID: `54843073` — use this always
- `target: "trevorpope"` (username) fails with 400 Bad Request

### `openclaw message send` correct flags
```bash
openclaw message send --channel telegram --target 54843073 --message "text"
# WRONG: --to and --text are NOT valid flags
```

### message tool requires explicit channel
- Always pass `channel: "telegram"` explicitly when multiple channels configured

### Telegram alert fallback chain (isolated crons)
1. `message` tool (fails — no bot token in isolated sessions)
2. `openclaw message send --channel telegram --target 54843073 --message "..."` CLI
3. Read bot token from `~/.openclaw/clawdbot.json` via exec + raw curl to Telegram API

---

## GOG (GOOGLE WORKSPACE) GOTCHAS

### gog gmail commands
- `gog gmail archive <messageId>` — correct top-level verb
- NOT `gog gmail messages archive` — returns "unexpected argument archive"
- `gog gmail attachment download`: flag is `--out <path>` NOT `-o`

### gog drive commands
- `gog drive download <fileId>`: fileId is positional NOT `--id`; use `--out` not `--output`
- `gog drive delete <fileId>`: correct command to trash; NO `gog drive trash` subcommand
- Download without `--format`: omit format flag entirely (xlsx downloads native)

### gog keychain issues
- Homebrew updates invalidate stored signing identity
- Permanent fix: `gog config set keyring file` then re-auth each account
- `aes.KeyUnwrap(): integrity check failed` = corrupt token → `gog auth add <account>` to re-auth

### Google Sheets API disabled
- `gog sheets` commands fail "Sheets API has not been used in project 578215858596"
- Workaround for reads: `gog drive download <fileId>` as CSV
- IMPORTRANGE only works between NATIVE Google Sheets (not XLSX files in Drive)

---

## OPENCLAW UPDATE GOTCHAS

### Cannot update from inside gateway
- `openclaw update` fails with "Package updates cannot run from inside the gateway service process"
- Crons always run inside gateway → PATCH cron using this = always fails
- Must trigger from external terminal or launchd

### Update confirmation
- `gateway update.run` returns `status: "skipped", reason: "managed-service-handoff-started"` even on success
- This is NOT confirmation of install — verify with `openclaw --version`

### Session banner lag
- After update + restart, session banner shows OLD version for one restart
- Always confirm version via `openclaw --version` CLI

### Update SIGKILL behavior
- Running `openclaw update` from exec: process killed mid-run when gateway restarts
- Expected — NOT an error. Check `openclaw --version` after

---

## PYTHON / SCRIPT GOTCHAS

### pytz not installed
- Host Python environment doesn't have `pytz`
- Use stdlib: `from datetime import datetime, timezone`
- If pytz truly needed: `pip3 install pytz` first

### openpyxl row deletion
- Deleting a row does NOT auto-update formulas referencing row numbers
- Scan and regenerate all formulas in summary tabs after any row deletion

### subprocess curl vs urllib for Cloudflare Workers
- `urllib.request.urlopen` fails for Cloudflare Workers (GET errors, POST returns 403)
- Use `web_fetch` for reads, `subprocess curl` for POST

### Python-edited HTML: Unicode quote corruption
- Python regex can replace ASCII `"` with curly Unicode quotes inside JS literals
- Browsers silently refuse to execute the `<script>` block
- Post-edit check:
```bash
python3 -c "
import re, sys
with open(sys.argv[1]) as f: content = f.read()
bad = re.findall(r'[\u201c\u201d\u2018\u2019]', content)
print(f'Unicode quotes found: {len(bad)}' if bad else 'Clean')
" path/to/file.html
```

### JS HTML injection — string replacement anchor
- Don't anchor `str.replace()` to `</script>` — inserts code AFTER the closing tag, silently ignored
- Anchor to content INSIDE the `<script>` block, or use `re.sub(r'(</script>)', new_js + r'\1', html, count=1)`

### Monday.com scripts with 500 items
- `monday-email-cleanup.js` / `monday-status-updater.js` process 500+ rows
- Use `exec timeout: 120, yieldMs: 90000` — NOT shell-level `timeout`/`gtimeout` (not on macOS)

---

## MISCELLANEOUS GOTCHAS

### `image` tool on this host: sharp missing
- `image` tool fails with "Cannot find package 'sharp'"
- Fallback: `read` tool directly on JPEG/PNG files — works natively

### inbound media path outside sandbox
- `image` tool rejects `~/.openclaw/media/inbound/` paths
- Workaround: `cp "$IN" ~/clawd/tmp/` then pass workspace path to `image` tool

### wrangler KV — always pass --remote
- `wrangler kv key list --namespace-id <id>` without `--remote` reads local/preview binding
- For prod KV: always add `--remote`

### Cloudflare Pages: corrupted file recovery
```bash
curl -s https://milobot.io/<page>/ > ~/clawd/sites/<page>/index.html
```

### macOS `systemsetup -setsleep` Error:-99
- Prints `### Error:-99` from AppleInternal Admin framework even when command succeeds
- Check next line for success confirmation — don't report as failure

### UPS tracking: blocked
- `web_fetch` fails, browser SSRF policy blocks hostname navigation
- Just send Trevor: `https://www.ups.com/track?tracknum=<number>`

### Lovable MCP OAuth
- Lovable MCP OAuth only works in Claude Desktop and Cursor
- OpenClaw can't complete the handshake
- Workaround: use `browser` tool with `profile="user"` to control Lovable web UI directly

### wrangler KV list: --remote required
- Without `--remote`: returns `[]` (reads local/preview binding)
- For prod: `wrangler kv key list --namespace-id <id> --remote`

---

## MEMORY / STATE PATTERNS

### Memory files
- `~/clawd/MEMORY.md` — long-term curated (main session only, <100 lines)
- `~/clawd/memory/session-state.md` — rolling handoff
- `~/clawd/memory/YYYY-MM-DD.md` — daily logs
- Write to memory: use `write` for new daily files (not `edit`), use `edit` only for existing files

### Context window management
- At >60%: push work to subagents
- At >75%: write to memory files BEFORE compaction
- After compaction: read `session-state.md` first, never ask "what were we doing?"

### Bootstrap truncation
- TOOLS.md (large file) is injected at ~12KB cap — only ~37% visible per bootstrap
- If a cron or heartbeat needs a specific section, read the file directly via `read path=TOOLS.md`

### mem0 semantic search
```bash
~/clawd/scripts/mem0 search "<query>"       # fuzzy/contextual
~/clawd/scripts/mem0 add "<fact>"           # add to vector store
```
