Device Prep and Runtime
Purpose
Define what must be true before a skill run is reliable: device readiness, wrapper environment, timeout behavior, multi-device targeting, and output capture.
Sources
- Skill runtime:
apps/node/src/domain/skills/runSkill.ts - Skill config:
apps/node/src/domain/skills/skillsConfig.ts - CLI skill wrapper:
apps/node/src/cli/commands/skills.ts - Environment variables overview:
docs/api/environment.md
Device Prep Checklist
Before clawperator skills run, the target should already satisfy the normal Clawperator readiness path:
- device visible to adb
- expected Operator package installed
- accessibility service running
- version compatibility healthy
- interactive device state healthy
Recommended verification:
clawperator doctor --device <device_serial>
Treat the device as ready only when:
- exit code
0 criticalOk == truechecksincludesreadiness.device.interactivewithstatus: "pass"
Verification pattern:
- run
clawperator doctor --device <device_serial> --operator-package <package> - confirm
report.operatorPackagematches the package you plan to use inside the skill - confirm the doctor report is for the same
deviceIdyou plan to pass toskills run
High-level wrapper enforcement now matches that readiness contract:
clawperator skills runresolves the target tuple, checks the requested Operator package first, and when the target is asleep it makes a bounded host-side wake attempt before failing withDEVICE_NOT_INTERACTIVEPOST /skills/:skillId/runin serve performs the same pre-spawn readiness preparation
Current bounded wake behavior:
- if the device is already awake and ready, the wrapper proceeds immediately
- if the screen is off, the wrapper tries host-side wake in the internal retry order and then re-checks readiness
- if the device is awake but still locked, or Android still reports
userUnlocked == false, the wrapper fails withDEVICE_NOT_INTERACTIVE - the wrapper does not attempt credential entry or secure-keyguard bypass
Wrapper-level non-interactive failure shape:
{
"code": "DEVICE_NOT_INTERACTIVE",
"message": "Device is not interactive. Interactive automation requires an awake, usable device state."
}
Use clawperator doctor when you need the structured evidence fields
(screenOn, deviceLocked, userUnlocked) that explain why the device is not
ready.
For first-time setup, use Setup. For runtime recovery, use Operator App.
Runtime Environment Passed To Skill Scripts
The wrapper injects these environment variables:
| Variable | Meaning |
|---|---|
CLAWPERATOR_BIN |
command the skill should use when it needs to invoke Clawperator |
CLAWPERATOR_OPERATOR_PACKAGE |
Operator package the skill should target on its internal CLI calls |
CLAWPERATOR_DEVICE_ID |
explicit device selection only; present only when the caller passed --device or serve deviceId |
Resolution behavior:
CLAWPERATOR_BINuses explicit env override first, then a local sibling build, then globalclawperatorCLAWPERATOR_OPERATOR_PACKAGEuses the explicit wrapper flag first, then environment, thencom.clawperator.operator
The wrapper preserves the rest of process.env when spawning the child process, but it explicitly clears inherited CLAWPERATOR_DEVICE_ID unless the caller passed an explicit device selection.
The exact CLAWPERATOR_BIN resolution order from resolveSkillBin() is:
- non-empty
CLAWPERATOR_BIN - branch-local sibling build at
apps/node/dist/cli/index.jswhen it exists - global
clawperator
The exact CLAWPERATOR_OPERATOR_PACKAGE resolution order from cmdSkillsRun() plus resolveOperatorPackage() is:
--operator-package <pkg>onclawperator skills run- non-empty
CLAWPERATOR_OPERATOR_PACKAGE com.clawperator.operator
Verification pattern:
CLAWPERATOR_OPERATOR_PACKAGE=com.clawperator.operator.dev \
clawperator skills run com.android.settings.capture-overview
CLAWPERATOR_OPERATOR_PACKAGE=com.clawperator.operator.dev \
clawperator skills run com.android.settings.capture-overview --operator-package com.clawperator.operator
Then verify the skill's internal clawperator calls behave against the intended package. When you need stronger confirmation, add a deliberate internal probe inside the skill script and inspect the raw output.
How Device Id Is Passed
runSkill() itself does not invent a device id. The CLI wrapper decides argument passing.
Current high-level wrapper behavior:
- both CLI
skills runand servePOST /skills/:skillId/runmay resolve a target device for readiness preflight even when the caller omitted explicit device selection - that implicit resolution is preflight-only
- if
--device <serial>or servedeviceIdis present, the wrapper writes that explicit value intoCLAWPERATOR_DEVICE_ID - for script-driven skills,
runSkill()then prependsCLAWPERATOR_DEVICE_IDas the first child argument - then the wrapper appends any forwarded args after
--
That means most scripts should expect:
argv[2] = <device_serial>
when they are run through the wrapper with explicit device targeting.
Verification pattern:
clawperator skills run com.example.app.do-thing --device <device_serial> -- --mode smoke
Confirm that your script receives:
- first positional child argument:
<device_serial> - remaining forwarded args after that:
--mode,smoke
If --device is omitted, the wrapper still may resolve a single connected device for preflight, but it passes no synthetic device argument into the child process.
Timeout Behavior
Default skill timeout:
120000milliseconds
This comes from runSkill.ts and applies to the subprocess wrapper, not to any single Clawperator action inside the skill.
Override it with:
clawperator skills run <skill_id> --timeout 90000
If the child does not exit in time:
- the wrapper sends
SIGTERM - the command fails with
SKILL_EXECUTION_TIMEOUT
Top-level timeout parsing failures happen before the skill starts:
{
"code": "EXECUTION_VALIDATION_FAILED",
"message": "timeoutMs must be a finite number"
}
{
"code": "USAGE",
"message": "--timeout requires a value"
}
Verification pattern:
clawperator skills run com.android.settings.capture-overview --timeout 3210
Check:
timeoutMsis3210in the success payload- if you omit
--timeout, the JSON payload does not echotimeoutMs, but the wrapper still uses the internal default120000
Multi-Device Skill Execution
When more than one device is connected:
- always pass
--device <serial>
Example:
clawperator skills run com.android.settings.capture-overview --device <device_serial>
Why:
- the wrapper uses the resolved target device for pre-spawn readiness
- the wrapper forwards only explicit caller-supplied device selection into the child script
- the child script can then pass that same serial into its internal Clawperator calls
Without explicit targeting, skill behavior depends on what the script itself does. The wrapper does not auto-add a device argument unless one was provided.
Related failure mode:
- if the child script later invokes Clawperator without a device while multiple devices are connected, the nested call can fail with
MULTIPLE_DEVICES_DEVICE_ID_REQUIRED
Recovery:
- pass
--device <serial>on the outerskills run - ensure the child script forwards that positional device id into its own internal Clawperator calls
Output and Logging
runSkill() captures:
- stdout
- stderr
- exit code
- total duration
Success JSON exposes the parsed SkillResult and timing:
| Field | Meaning |
|---|---|
skillResult |
parsed structured skill result |
durationMs |
total wrapper runtime |
Default JSON success responses omit status, skillId, exitCode, and
output at the top level and expose the answer under skillResult.result (see
the table in Skill result trust order and JSON wrapper
policy).
Failure wrapper fields may also include:
| Field | Meaning |
|---|---|
stdout |
partial stdout captured before failure |
stderr |
partial stderr captured before failure |
exitCode |
non-zero child exit code when available |
JSON responses for clawperator skills run include a logs object. The CLI
creates it before validation and readiness preflight, so early failures can
still be correlated with the daily log when file logging is available:
| Field | Meaning |
|---|---|
logs.skillRunId |
correlation id for this invocation |
logs.path |
daily NDJSON log file path |
logs.tailCommand |
ready-to-run command for observing that log file |
The same skillRunId is also available to the skill script as
CLAWPERATOR_SKILL_RUN_ID. Nested Clawperator CLI calls inherit it. When those
calls use the daemon, the id is sent on the individual daemon request so
request-specific execution logs can correlate to the skill run without making
the long-lived daemon process inherit stale per-run state.
In pretty mode, the CLI also prints a one-line banner with:
- CLI version
- APK status
- log path
- docs hint
That banner is a convenience layer from cmdSkillsRun(), not part of the JSON wrapper contract.
The pretty banner contains these exact components:
- CLI version from
getCliVersion() - APK status derived from
checkApkPresence() - log path, defaulting to
~/.clawperator/logs/clawperator-YYYY-MM-DD.logwhenlogger.logPath()did not supply an override - hint:
tail -f <logPath> - docs hint:
https://docs.clawperator.com/llms.txt
Verification pattern:
- use the default JSON output when another tool needs parseable output
- use pretty mode when a human operator wants the banner plus streamed skill output
- if pretty mode shows an APK warning or failure, fix the package/device setup before assuming the skill logic is wrong
Skill result trust order and JSON wrapper policy
Trust order (not the same as JSON key order):
- Branch on the top-level wrapper
statusandcode(and HTTPokfor serve) first. When the wrapper isindeterminateorfailed, do not treat nestedskillResult.statusas the primary success signal. - After the wrapper is
success(or you are intentionally reading child state), inspect nestedskillResult.statusas child-authored reported state. The runtime may still mark the wrapperindeterminateif declared verification is not proved while the child reported success. - Read the domain answer from
skillResult.result. Useresult: nullonly when no truthful domain value exists. - Use
checkpoints,terminalVerification, andexecEnvelopesas proof and audit, not as the default answer path. Do not direct agents to discover the primary return value only through checkpoint ids,terminalVerification, ordiagnostics. diagnosticsis for runtime health, warnings, hints, timings, and debug detail only, not a second copy of the answer.
Nested field order in examples and emitted JSON (human and agent scanability): inside the nested skillResult object, result first, then status, then the remaining proof and metadata fields. Object key order is not required for parsers, but it is the documented authoring convention.
Default JSON for clawperator skills run and POST /skills/:skillId/run:
| Wrapper path | skillResult |
Top-level status |
skillId |
exitCode |
output |
|---|---|---|---|---|---|
| Success | not null |
omitted | omitted | omitted | omitted |
| Indeterminate (verification not proved) | not null |
present | omitted | omitted | omitted |
| SKILL_OUTPUT_ASSERTION_FAILED | may be not null |
error shape | present in error | n/a for success | present (diagnostic; shows what the skill printed) |
| Execution / parse / other failures | often null |
error | varies | in error when relevant | n/a; failures use stdout and stderr for process streams |
The additive logs object is present alongside these wrappers when a skill run
was started. It is not a second output channel and does not change the daily log
file location.
Pretty mode can show skillId, exitCode, and streamed output for operators;
the JSON deduplication policy applies when --output json or the serve JSON
body for skill runs is used.
Debugging skill runs with logs:
# Stream logs in real time while a skill runs
clawperator logs
# In another terminal:
clawperator skills run <skill_id> --device <device_serial> --operator-package <package>
The unified logger captures skill output as skills.run.output events, enabling post-timeout diagnostics. See Logging for details. Each run also emits a skills.run.log_location event with the same logs.skillRunId and daily file path.
Runtime success examples
Success (default JSON): top level has only the nested object plus timing;
no duplicate status, skillId, exitCode, or output. The domain
answer is under skillResult.result.
{
"skillResult": {
"result": {
"kind": "text",
"text": "ok"
},
"status": "success",
"contractVersion": "1.0.0",
"skillId": "com.example.app.read-metric",
"goal": { "kind": "read_metric" },
"inputs": {},
"checkpoints": [],
"terminalVerification": { "status": "verified" },
"diagnostics": { "runtimeState": "healthy" },
"source": { "kind": "script" }
},
"logs": {
"skillRunId": "skillrun_1777600000000_00000000-0000-4000-8000-000000000000",
"path": "/home/user/.clawperator/logs/clawperator-2026-03-28.log",
"tailCommand": "tail -f '/home/user/.clawperator/logs/clawperator-2026-03-28.log'"
},
"durationMs": 15321
}
Runtime indeterminate example
When verification is not proved, default JSON omits
skillId, exitCode, and output at the top level. Wrapper status, code,
and message stay because they carry distinct wrapper state.
{
"status": "indeterminate",
"code": "SKILL_VERIFICATION_INDETERMINATE",
"message": "Declared verification was not proved.",
"skillResult": {
"result": { "kind": "json", "value": { "note": "example" } },
"status": "success",
"contractVersion": "1.0.0",
"skillId": "com.android.settings.capture-overview",
"checkpoints": [],
"source": { "kind": "script" }
},
"durationMs": 15321
}
This wrapper-level indeterminate state means the skill process ran without an
upstream runtime failure, but the declared skill.json verification contract
was not proved. The parsed skillResult is returned verbatim; the wrapper
does not rewrite it.
Runtime Failure Example
{
"status": "failed",
"code": "SKILL_EXECUTION_TIMEOUT",
"message": "Skill com.android.settings.capture-overview timed out after 120000ms",
"skillId": "com.android.settings.capture-overview",
"stdout": "RESULT|status=partial\n",
"stderr": "still waiting for target node\n",
"skillResult": null
}
Another common failure is a bad registry or missing script:
{
"code": "REGISTRY_READ_FAILED",
"message": "Registry not found at configured path: /tmp/missing-registry.json. The installed registry normally lives at ~/.clawperator/skills/skills/skills-registry.json. Fix CLAWPERATOR_SKILLS_REGISTRY, unset it to use the installed copy, then rerun clawperator skills list, or run clawperator skills install."
}
{
"code": "SKILL_SCRIPT_NOT_FOUND",
"message": "Script not found: /abs/path/to/skills/com.android.settings.capture-overview/scripts/run.js",
"skillId": "com.android.settings.capture-overview"
}
{
"code": "SKILL_NOT_FOUND",
"message": "Skill not found: com.android.settings.capture-overview",
"skillId": "com.android.settings.capture-overview"
}
Recovery patterns:
REGISTRY_READ_FAILED: runclawperator skills installto restore the registry at the installed home path (~/.clawperator/skills/skills/skills-registry.json); ifCLAWPERATOR_SKILLS_REGISTRYis set to a custom path, fix or unset that variableSKILL_NOT_FOUND: confirm the exact registryidwithclawperator skills listSKILL_SCRIPT_NOT_FOUND: repair the registry entry or restore the script file on diskSKILL_EXECUTION_FAILED: inspectexitCode,stdout, andstderrSKILL_EXECUTION_TIMEOUT: inspect partialstdoutand only then consider increasing--timeoutSKILL_RESULT_PARSE_FAILED: fix malformed framed output or unreadable trusted source metadata inskill.json
Practical Runtime Rules
- gate device readiness with
doctorbefore blaming the skill - pass
--deviceexplicitly in multi-device environments - pass
--operator-package com.clawperator.operator.devfor local debug APK workflows - prefer default JSON output for machine-consumed skill runs so the pretty banner does not pollute stdout
- inspect partial stdout and stderr on failures before rerunning blindly