T09 · Insecure Skill Coding Practices
- Location
scripts/heartbeat-to-poke.sh:153- Finding
Shell Injection in Generated Heartbeat Migration Commands
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
This reminder scheduler is mostly disclosed and purpose-aligned, but its persistent scheduled execution and command-generation code have concrete safety issues that should be reviewed before installation.
Review this skill carefully before installing, especially on macOS or Windows despite the Linux-only wording. Use it only if you want user-level persistent scheduled jobs, avoid command hooks unless the scripts are trusted, avoid executing generated migration commands from untrusted heartbeat config, and inspect or remove created reminders with the management commands when no longer needed.
scripts/heartbeat-to-poke.sh:153Shell Injection in Generated Heartbeat Migration Commands
src/scheduler.js:189Windows Scheduled-Task Command Injection Through Environment Values
The supplied code does not implement the skill's main declared behavior of creating persisted OS-native timers or running reminder hooks. Instead, it is a narrow macOS entry point for the opt-in heartbeat-to-poke migration helper. That helper is mentioned in the description, so the migration-related behavior itself is not undeclared; however, the description emphasizes Linux-only support while this script explicitly targets macOS, which is a material inconsistency. Therefore this chunk is mismatched relative to the declared purpose.
The declared description presents a full scheduler skill whose primary function is to create persisted OS-native timers and support rich runtime features like command hooks, reply-awareness, escalation, quiet hours, and optional Ollama integration. The supplied code chunk does not implement that scheduler. Instead, it is specifically a conversion/migration utility: it parses openclaw.json, HEARTBEAT.md, and optional heartbeat state, then prints equivalent poke commands to stdout. Its only state-changing behavior is the opt-in --apply patch that disables native heartbeat in openclaw.json with a backup, which matches one narrow part of the description. Quiet-hours/active-hours are only translated into printed flags, not enacted by creating native timers. Since the primary purpose and several key capabilities in the description are absent from the code, this is a material description-behavior mismatch.
The core declared purpose is mostly accurate: this code clearly implements persisted reminders/tasks, reply-aware handling, escalation, quiet hours/DND, and optional command hooks and vector-tone matching. However, there are material description/behavior gaps. First, the code contains substantial additional capabilities not mentioned in the description: commitment tracking with its own persisted store and CLI, plus path-set storage and placeholder expansion. These are user-facing features, not merely internal implementation details. Second, a notable declared feature—the opt-in --apply migration helper that rewrites OpenClaw config with backup—is absent from this chunk. Third, the description emphasizes Linux-only support, while the code itself does not enforce Linux-only operation and references cross-platform behavior/backend abstraction, making the platform description inaccurate relative to observed behavior. Therefore the description does not fully and accurately represent the supplied code chunk.
The declared description is for a full scheduler/poke system with native timer creation, command execution, optional migration, and optional Ollama integration. The supplied code chunk does none of that. It is clearly a test helper: an in-memory mock runtime used under POKE_RUNTIME_BACKEND=mock, with stub methods that only record calls and return canned values. Its primary purpose is materially different from the declared skill behavior, so this is a strong mismatch.
The declared description presents a scheduling/reminder skill whose main function is to create native OS timers and later run commands/prompts. The supplied code does not implement scheduling or timer persistence at all. Its actual purpose is a backend runtime wrapper for OpenClaw: resolving binary/config paths, generating a temporary compatibility config, executing openclaw agent --local / agents list --json, retrying transient failures, and exposing direct CLI access. While some mention of OpenClaw appears in the description, the specific claimed behaviors in the description are absent from this code chunk, and the code instead provides a different subsystem-level capability (agent runtime transport). Therefore the description does not accurately represent this code chunk.
The primary purpose broadly matches: this code is clearly for creating persisted OS-native scheduled executions without a daemon, using systemd/launchd/schtasks and running a later delivery command under the user context. However, the description materially misstates platform availability: the code already contains active macOS and Windows implementations, not merely future plans. Additionally, the chunk only schedules a fixed delivery invocation (node <script> --deliver <reminderId>) and passes a few environment variables; it does not show support for the declared configurable command hooks. The migration helper and vector-tone networking features are also absent from this chunk, though those may exist elsewhere. Because the stated platform support is directly contradicted by the code and some declared capabilities are not represented here, this should be flagged as a mismatch.
The comment says "Poke NEVER ... inspects the reply," yet the file contains reply-processing logic that reads inbound reply text, heuristically and agent-classifies it, and then confirms, cancels, snoozes, or triggers pong commands. This is an active contradiction between inline documentation and implemented behavior.
The systemd service runs /bin/sh -c with a compound command that chains delivery, timer disablement, file deletion, and daemon reload using shell metacharacters. Although the individual interpolated values are partly escaped and reminder IDs are sanitized, using a shell at all expands the attack surface and makes the persistence cleanup path brittle; if upstream values such as scriptPath or future arguments bypass escaping, this becomes command-injection with arbitrary code execution under the user account.
.map(([key, value]) => `Environment=${systemdEscape(`${key}=${value}`)}`);
const unitBase = handle.unitName;
const deliverCmd = [process.execPath, scriptPath, "--deliver", reminderId].map(systemdEscape).join(" ");
const cleanupCmd = `systemctl --user disable --now ${shellEscape(`${unitBase}.timer`)} 2>/dev/null; rm -f ${shellEscape(handle.servicePath)} ${shellEscape(handle.timerPath)}; systemctl --user daemon-reload`;
return [
"[Unit]",
`Description=Text reminder: ${reminderId}`,
Skill manipulates agent memory, state, or stored context. Memory corruption can alter personality, override safety rules, or cause unpredictable behavior.
These bypass the reply classifier (no need for `--reply`). Use them
when the operator (you, the agent, or a script) needs to clear state
directly — not when a user's chat message arrives (that's `--reply`).
## History & stats
The skill advertises powerful capabilities including filesystem writes, process execution, environment access, and optional network use, but it does not declare an explicit restrictive tool scope such as permissions or allowed-tools. In a security-sensitive agent ecosystem, this weakens policy enforcement and can let a broadly described skill be invoked with more authority than reviewers or users expect.
The invocation guidance uses broad everyday phrases like 'remind me', 'wake me', 'every morning', and 'keep poking until I reply', which can cause the skill to trigger in ordinary conversation more often than intended. Because this skill can create persisted schedulers and later execute commands or prompts under the user account, accidental invocation has a materially higher security and safety impact than a read-only skill.
The manifest describes a scheduler that persists OS-native timers, runs configured hooks, and optionally uses vector-tone matching. In this file, the --paths-save/--paths-show/--paths-delete features and {{paths:name}} expansion add a separate capability for storing and injecting arbitrary file paths into reminder/task content, which is not disclosed in the manifest description.
The manifest frames the skill as a time-based reminder scheduler with hooks, reply handling, quiet hours, migration helper, and vector-tone mode. This file also implements a separate persisted commitments feature (--commit, --commitments, --commit-done, --commit-cancel) and includes due commitments in task prompts, materially expanding the skill's behavior beyond the stated reminder purpose.
This code executes persisted user-configured commands automatically via execFileSync during reminder delivery, but there is no runtime consent prompt or execution-time warning when those hooks fire. In this skill's context, reminders are scheduled to run later under the user's account via OS-native timers, so a user may forget hooks were attached and end up with unattended command execution, which materially raises the risk of destructive local actions.
The manifest says the skill is 'Linux only for now; mac coming VERY soon. Windows eventually.' However, backend detection explicitly selects launchd on macOS and schtasks on Windows, and the rest of the file contains concrete create/remove/query implementations for both. This is a direct mismatch between the documented platform scope and actual behavior.
This code constructs a LaunchAgents plist path under the user's Library, enabling persisted execution via launchd. In the context of a scheduler skill, persistence is intentional, but it still creates an autorun mechanism under the user's account that can be abused if reminders or command hooks are created without sufficiently explicit user approval.
return {
backend,
label: `ai.openclaw.poke.${safeId}`,
plistPath: path.join(os.homedir(), "Library", "LaunchAgents", `ai.openclaw.poke.${safeId}.plist`)
};
case "schtasks":
return {
The function builds a launchd plist that will later execute the reminder delivery command. Generating persistence artifacts is core functionality here, but launchd registration creates durable execution capability that increases risk if upstream inputs or command hooks are abused.
.join("\n");
}
function buildLaunchdPlist(handle, dueAt, scriptPath, reminderId, stateDir, envPairs) {
const interval = buildLaunchdCalendarParts(dueAt);
const envXml = Object.entries(envPairs)
.filter(([, value]) => value)
This finding is a duplicate match on the same plist-emission site. The relevant risk is persisted execution via launchd, not XML injection, since values are escaped.
const logDir = path.join(stateDir, "logs");
ensureDirectory(logDir);
return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
This finding is a duplicate match on the same plist-emission site. The relevant risk is persisted execution via launchd, not XML injection, since values are escaped.
const logDir = path.join(stateDir, "logs");
ensureDirectory(logDir);
return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
This is another match within the launchd plist generation block. The danger comes from establishing a persistent scheduled execution path that survives beyond the current process and can run later without interactive review.
ensureDirectory(logDir);
return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${escapeXml(handle.label)}</string>
This location is still part of the plist builder and therefore part of the same persistence mechanism. Persistent scheduler registration is not inherently malicious, but it is security-sensitive because it creates future execution capability under user context.
${envXml}
</dict>
</dict>
</plist>
`;
}
This code creates and removes persistent scheduler artifacts across systemd, launchd, and Windows Task Scheduler using file writes and deletions, but it does so silently with stdio suppressed and without any confirmation prompt, logging, or explanatory comment/docstring. These operations affect system configuration and persistence, which matches the safety-critical file-write/deletion category for code files.
The file invokes systemctl, launchctl, and schtasks to register, restart, stop, disable, and delete scheduled jobs, all with output suppressed and no user-facing warning or nearby explanatory documentation. Because subprocess execution changes system scheduler state and persistence behavior, it should include some form of disclosure unless explicitly documented elsewhere.
The code writes the launchd plist to disk, which is the concrete installation step for persistence. In this skill's context, that behavior is expected, but it still materially increases risk because a malicious or mistaken upstream reminder configuration becomes a durable autorun artifact.
}
function upsertLaunchd(handle, dueAt, scriptPath, reminderId, envPairs, stateDir) {
ensureDirectory(path.dirname(handle.plistPath));
fs.writeFileSync(handle.plistPath, buildLaunchdPlist(handle, dueAt, scriptPath, reminderId, stateDir, envPairs));
const domain = detectGuiDomain();
execBestEffort("launchctl", ["bootout", `${domain}/${handle.label}`]);
This is a duplicate pattern match on the same launchd installation flow. The substantive issue remains persistent scheduled execution via LaunchAgents.
function upsertLaunchd(handle, dueAt, scriptPath, reminderId, envPairs, stateDir) {
ensureDirectory(path.dirname(handle.plistPath));
fs.writeFileSync(handle.plistPath, buildLaunchdPlist(handle, dueAt, scriptPath, reminderId, stateDir, envPairs));
const domain = detectGuiDomain();
execBestEffort("launchctl", ["bootout", `${domain}/${handle.label}`]);
execBestEffort("launchctl", ["bootout", domain, handle.plistPath]);
Detected: suspicious.dangerous_exec