Skip to content

Debugging & Troubleshooting

The plugin runs inside the host process with no standalone daemon, so troubleshooting differs a bit from debugging the standalone CLI: first confirm the plugin is installed correctly, then confirm the Relay tunnel is connected, and finally check logs to pin down the specific step.

Step 1: run doctor

bash
openclaw ntf doctor
openclaw ntf doctor --json
openclaw ntf doctor --fix

doctor actually runs 9 checkers in priority order:

LevelCheckerWhat it checksAuto-fixable
criticaldangerous-flagsWhether gateway.controlUi.dangerouslyDisableDeviceAuth is trueYes (writes config back, keeps a .bak)
criticaltrusted-proxyWhether gateway.auth.mode is trusted-proxy (a real security risk)No, needs manual confirmation
criticalcredentialsWhether the api-key is configuredNo, prompts to run ntf auth set-api-key
warntrusted-proxy-allow-usersWhether trustedProxy.allowUsers is emptyNo
warnstate-dir-permsWhether the state directory's permission bits are open to group/otherYes (chmod 700)
warntool-policyWhether any agent still uses the default (permissive) tool profileNo
warntunnelCurrent Relay tunnel status (see below)No
infonode-versionWhether Node is older than 22.12.0No
infoplugin-versionWhether a newer plugin version is availableNo (suggests ntf update)

Note: doctor does not check whether workspaceDir/stateDir is writable, does not check local ASR dependencies (whisper-cli/opusdec/ffmpeg), and does not check whether the Tailscale / external remote-gateway mutual-exclusion path has kicked in. These three classes of issue currently need to be diagnosed manually per the "common symptoms" section below — don't expect doctor to surface them.

The tunnel checker reports one of four states:

StateMeaning
STATUS_NOT_FOUNDThe tunnel status file doesn't exist (the plugin has never connected successfully, or the state dir was cleared)
STATUS_INVALIDThe tunnel status file failed to parse (corrupted)
STATUS_STALEThe status file claims "connected" but the local lock file is missing/stale/its owning process is dead
"隧道未连接 (state=...)"Carries lastDisconnectReason, explaining the most recent disconnect

Step 2: confirm the Relay tunnel

bash
openclaw ntf auth show          # is the api-key in place
openclaw ntf tunnel-status      # tunnel connection status

When connected, tunnel-status looks roughly like:

json
{ "ok": true, "connected": true, "reconnectAttempt": 0,
  "relayUrl": "wss://openclaw-service.yoooclaw.com/..." }
FieldMeaning
connectedWhether the WebSocket is established and heartbeating
reconnectAttemptCumulative reconnect count; 0 means it has never dropped
lastDisconnectReasonReason for the most recent disconnect

If connected stays false, check two things first: whether the api-key is configured (openclaw ntf auth show), and whether the host has gateway.tailscale.mode or an external gateway.remote.url configured — when either is true, the plugin deliberately skips starting the Relay tunnel at startup, logging a line like Relay tunnel: detected ... in host gateway config, skipping relay startup to keep ingress mutually exclusive. That's by design, not a failure. Additionally, the moment an external, directly-addressed POST /notifications request arrives (i.e. not internal Relay-forwarded traffic), the plugin will proactively tear down an already-established Relay tunnel to avoid running two ingress paths at once.

If two plugin processes on the same machine both try to connect to Relay, a relay-tunnel.lock file (recording pid/startedAt) mutually excludes them: whichever process starts second sees the lock owner is still alive and silently skips connecting — no error — logging another local process already owns the tunnel lock.

Step 3: check logs

bash
openclaw ntf log --keyword relay
openclaw ntf log --keyword error --limit 100

Logs are written under plugins/phone-notifications/logs/YYYY-MM-DD.log in the host state directory; find the exact path with openclaw ntf storage-path. Logs are retained for 30 days by default. Production (non-beta) builds redact log content: API keys/tokens/passwords, message bodies, sender names, transcripts, URLs, emails, phone numbers, JWTs, and long hex strings are all replaced with placeholders like [redacted] / [redacted-secret] / [redacted-url]. If you can't see notification content in the logs, that's expected behavior, not data loss — to see unredacted logs, switch to the beta channel first with openclaw ntf update --beta.

Key log-line meanings mirror the log-line table in the standalone CLI debugging doc — both share the same RelayClient implementation, so the log format is largely identical.

Common error messages

These are the literal error strings the plugin code throws or returns — searchable keywords to locate the cause quickly:

Error messageWhen it happensWhat to do
API Key 未设置,请先写入 <credentialsPath>,或通过宿主 CLI 执行 ntf auth set-api-key <apiKey>Reading credentials finds no apiKeyopenclaw ntf auth set-api-key <key>
apiKey 未设置,请先执行 ntf auth set-api-key <apiKey>(或 <credentialsPath>)Auth failed while establishing the Relay connectionSame as above; on JvsClaw the apiKey exchange retries indefinitely with exponential backoff until it succeeds or the plugin stops
未在 <configPath> 找到 link_secret(路径: plugins.entries.phone-notifications.config.link_secret)JvsClaw case: the host never injected link_secretCheck whether the host correctly delivered this config; it usually isn't set manually
instance/ready HTTP <status> / instance/ready 返回 code=... msg=...The JvsClaw apiKey-exchange endpoint erroredCheck network connectivity and JvsClaw control-plane status
通知存储目录不可用: <path>Neither stateDir nor workspaceDir has a writable notification storage directoryCheck write permissions on both; the plugin prefers stateDir and only falls back to workspaceDir if it's unwritable
录音存储目录不可用: <path>Same, for recordingsSame as above
录音不存在: <recordingId>Queried a recording ID that doesn't existConfirm the ID with ntf rec list
非法状态转换: <from> → <to>The recording state machine was called out of orderThe state machine is strictly linear and never allows skipping states; this is usually an internal bug rather than something a user can fix
light_control supports 1-N segmentslight send --segments exceeded the maximum segment countReduce the number of segments
repeat_times 必须是 >=0 的整数An invalid repeat_times value was passed to light controlFix the parameter
日志目录不可用 (LOGS_UNAVAILABLE)ntf log ran but the log directory was never createdThe plugin has never run successfully, or stateDir is misconfigured
workspaceDir and stateDir both unavailableA feature like monitors or light rules couldn't find any writable directoryCheck both directory paths and permissions
[whisper-local] 未找到 whisper.cpp 二进制文件。请安装 whisper.cpp 并确保 whisper-cli 在 PATH 中...asr.mode=local but whisper.cpp isn't installed locallyInstall it (e.g. brew install whisper-cpp) and make sure whisper-cli is on PATH
OGG/Opus 格式需要 ffmpeg 或 opus-tools(brew install opus-tools)Local ASR transcoding is missing a dependencyInstall ffmpeg or opus-tools
请安装 ffmpeg(brew install ffmpeg)或确保音频文件为 WAV 格式Same, for non-Opus formats missing ffmpegInstall ffmpeg

Common symptoms → fixes

ntf command not found after installing

First confirm the plugin actually registered:

bash
openclaw plugins list

If phone-notifications isn't in the list, go back to Installation and redo it; if another plugin already claims the ntf alias, use openclaw phone-notifications --help instead. The one-click install script also tries npm link to expose a bare ntf command; if that step fails (missing npm, npm link failing, or the linked binary not landing on PATH), it's not a fatal install error — it's only surfaced as a warning in the install summary and doesn't affect the plugin's own availability inside the host.

Commands don't take effect on QClaw

QClaw usually has no global openclaw command. After installing the plugin, restart the QClaw desktop app for it to take effect, then run the equivalent subcommand through QClaw's own wrapper.

Phone-side pushes aren't coming through

Check in order:

  1. openclaw ntf auth show — the api-key is configured;
  2. openclaw ntf tunnel-statusconnected: true;
  3. the host isn't simultaneously running Tailscale Funnel / an external remote gateway (both are mutually exclusive with the Relay tunnel, see above);
  4. openclaw ntf log --keyword ingest — confirm the request actually reached the plugin's ingest logic, rather than getting stuck at host gateway authentication;
  5. in the HTTP fallback mode, confirm the request itself is well-formed: any non-POST method fails with 405; a non-JSON body fails with 400 {"ok":false,"error":"Invalid JSON"}; a non-array top level fails with 400 {"ok":false,"error":"notifications must be an array"}; the storage service not being ready yet returns 503 {"ok":false,"error":"Service Not Ready"};
  6. if the request definitely arrived but the notification "disappeared," check whether ignoredApps filtered out that app's package name — both filtering and dedup (dedupedById/dedupedByContent) happen silently without an error, they just don't get persisted; the corresponding counts show up in the logs.

Light rules aren't triggering

Light rules are evaluated after being written to disk, asynchronously (1s debounce) — not synchronously the instant a notification arrives. First confirm the notification actually landed (openclaw ntf search --limit 1), then check the logs for a matching rule-evaluation entry; if the rule itself is disabled, evaluation is skipped.

A recording is stuck in one state

Recordings go through a state machine that doesn't allow skipping states. Check the current state first:

bash
openclaw ntf rec status <recording-id>

Stuck at sync_failed is usually a network or host gateway auth issue, and automatically falls back to syncing_openclaw to retry; stuck at transcribe_failed, check the dependency matching your asr.mode (local mode: check whisper-cli/ffmpeg/opusdec; api/hosted mode: check network and api-key), and it falls back to transcribing to retry. An ASR failure doesn't pollute the synced main chain and can be retried in place. receiving_failed can only fall back to receiving to re-receive — it never auto-advances to a later state.

Uninstalling the plugin

There's currently no dedicated uninstall command. To remove it, manually delete the plugin's install directory and remove the plugins.entries.phone-notifications entry from the host's openclaw.json, then restart the gateway. Re-running the install script is equivalent to an in-place upgrade (it automatically backs up the old version as <path>.bak.<timestamp>, keeping only the most recent backup).

Permission errors during installation

The installer gives targeted guidance:

  • EACCES/EPERM on Windows: usually means OpenClaw / QClaw / JvsClaw is still running and has files locked — fully quit the host first, then retry;
  • EACCES/EPERM on Unix: may mean a previous install used sudo, or the current user lacks write permission — close the host, then fix directory ownership or delete the old plugin directory and retry;
  • an existing install directory owned by root while the current user isn't root: aborts immediately with a hint that a previous install likely used sudo; fix the ownership first.

Next steps

  • Command Reference — all openclaw ntf subcommands.
  • CLI Debugging — both share the same Relay implementation, so many troubleshooting steps carry over.