Back to skill

Security audit

Claw Janitor

Security checks for vulnerabilities and agentic risk

Overview

This cleanup skill is not exfiltrating data, but it can delete broad local, Docker, and system resources by default with too little user control.

Review before installing. Use only with explicit dry-run-first workflows, avoid running it as root, and scope commands with --only or --skip. The publisher should make preview mode the default, require an explicit apply flag for mutation, and gate Docker and system cleanup behind explicit user authorization.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T09 · Insecure Skill Coding Practices

Warning
Location
janitor.js:271
Finding
Destructive cleanup executes by default without preview, confirmation, or explicit high-impact scope selection<![CDATA[ ## Vulnerability Details **File Location**: `janitor.js:271-326`, `janitor.js:373-376`; default invocation documented at `SKILL.md:27-28` **Vulnerability Type**: Unsafe destructive defaults and insufficient privilege scoping **Risk Level**: Medium ### Vulnerable Code ```javascript function cleanPackageManagers() { execCommand('npm cache clean --force', 'NPM Cache'); execCommand('yarn cache clean', 'Yarn Cache'); execCommand('pnpm store prune', 'PNPM Store'); execCommand('bun pm cache rm', 'Bun Cache'); removePath(path.join(NVM_DIR, '.cache'), 'NVM Version Zip Cache'); execCommand('pip cache purge', 'Python PIP Cache'); execCommand('go clean -cache -modcache', 'Go Module Cache'); removePath(path.join(HOME, '.cargo', 'registry', 'cache'), 'Rust Cargo Cache'); if (PLATFORM === 'win32') { removePath(path.join(process.env.LOCALAPPDATA || '', 'ms-playwright'), 'Playwright Browser Binaries (Win)'); removePath(path.join(process.env.LOCALAPPDATA || '', 'puppeteer'), 'Puppeteer Browser Binaries (Win)'); } else { removePath(path.join(CACHE_HOME, 'ms-playwright'), 'Playwright Browser Binaries'); removePath(path.join(CACHE_HOME, 'puppeteer'), 'Puppeteer Browser Binaries'); } removePath(path.join(os.tmpdir(), 'openclaw-tmp'), 'OpenClaw Temp Artifacts'); } function cleanDocker() { try { execSync('docker ps', { stdio: 'ignore', timeout: 5000 }); if (DEEP_CLEAN) { execCommand('docker system prune -a -f', 'Docker System Prune (All Unused Images & Containers)'); execCommand('docker builder prune -a -f', 'Docker Buildx/BuildKit Cache (Deep)'); } else { execCommand('docker system prune -f', 'Docker System Prune (Dangling Images Only)'); execCommand('docker builder prune -f', 'Docker Buildx/BuildKit Cache (Dangling)'); } } catch (e) { logAction('SKIP', 'Docker not found or daemon not run ...[truncated 4565 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Make preview mode the default and require an explicit `--apply` flag before any mutation. 2. Require explicit group selection for destructive categories, especially `--only docker` and `--only system`. 3. Never infer authorization for system cleanup solely from the process already being root. 4. Require a dedicated flag such as `--system-clean` for package autoremove and journal deletion. 5. Separate low-impact cache cleanup from resource deletion: - Keep cache cleanup in the default eligible set. - Place Docker container/network pruning, package autoremove, and journal vacuuming behind distinct options. 6. Remove forced confirmation flags such as `-y` and `-f` unless the user has explicitly approved the exact operation. 7. Correct the Docker description because `docker system prune -f` is not limited to dangling images. 8. Update `SKILL.md` so generic requests trigger a dry run first, followed by a summary and explicit user confirmation. 9. Add tests proving that: - A flagless invocation never mutates state. - Root execution does not implicitly enable system cleanup. - Docker and system operations require explicit authorization. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
janitor.js:231
Finding
PATH-based executable resolution permits command substitution, including during root cleanup<![CDATA[ ## Vulnerability Details **File Location**: `janitor.js:231-261`, with privileged call sites at `janitor.js:307-326` **Vulnerability Type**: Untrusted executable search path and shell-based command execution **Risk Level**: Medium ### Vulnerable Code ```javascript function hasCommand(bin) { try { if (PLATFORM === 'win32') { execSync(`where ${bin}`, { stdio: 'ignore', timeout: 5000 }); } else { execSync(`command -v ${bin}`, { stdio: 'ignore', timeout: 5000 }); } return true; } catch { return false; } } function execCommand(cmd, description, fallbackSize = 0) { const bin = cmd.split(' ')[0]; if (!hasCommand(bin)) { logAction('SKIP', `Command not installed: ${bin}`, 0); return; } try { if (DRY_RUN) { logAction('DRY-RUN', `Would execute: ${cmd} (${description})`, 0); return; } const output = execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf-8', timeout: 60000 }); let estimatedSize = fallbackSize; if (cmd.includes('docker') && output.includes('Total reclaimed space:')) { const match = output.match(/Total reclaimed space: ([\d\.]+) ([A-Z]+)/); if (match) { const val = parseFloat(match[1]); const unit = match[2]; if (unit === 'GB') estimatedSize = val * 1024 * 1024 * 1024; if (unit === 'MB') estimatedSize = val * 1024 * 1024; if (unit === 'KB') estimatedSize = val * 1024; } } logAction('EXEC', `${description}`, estimatedSize); totalSavedBytes += estimatedSize; } catch (e) { logAction('SKIP', `Command unavailable or failed: ${bin}`, 0); } } ``` Privileged operations check an absolute file but execute an unqualified command name: ```javascript if (fs.existsSync('/usr/bin/apt-get')) { execCommand('apt-get cle ...[truncated 3394 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace shell-based `execSync()` calls with `execFileSync()` and explicit argument arrays, for example: ```javascript execFileSync('/usr/bin/apt-get', ['clean'], options); execFileSync('/usr/bin/apt-get', ['autoremove', '-y'], options); ``` 2. Resolve every allowed executable to an absolute path before execution. 3. For root-only system operations, use fixed trusted paths such as `/usr/bin/apt-get` and `/bin/journalctl`, after validating ownership and permissions. 4. Do not perform an absolute-path existence check followed by basename execution. 5. Use a strict allowlist mapping command identifiers to executable paths and permitted argument arrays. 6. Reject executable paths located in directories writable by the invoking user or untrusted groups. 7. When elevated, replace the inherited `PATH` with a restricted value containing only trusted system directories. 8. Avoid invoking `command -v`, `where`, or a shell merely to test command availability; test the selected absolute executable directly. 9. Add regression tests with a temporary malicious executable placed first in `PATH` and verify that it is never executed. 10. Log the validated absolute executable path before running each external command so audit reports accurately identify the invoked binary. ]]>
Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (4)

Credential Access

High
Category
Privilege Escalation
Content
// 2) Regex protection (.env/.git)
  {
    const envPath = path.resolve('/tmp/project/.env');
    const gitPath = path.resolve('/tmp/project/.git/config');
    assert.strictEqual(isSafeToClean(envPath), false, '.env path must be blocked');
    assert.strictEqual(isSafeToClean(gitPath), false, '.git path must be blocked');
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
{
    const envPath = path.resolve('/tmp/project/.env');
    const gitPath = path.resolve('/tmp/project/.git/config');
    assert.strictEqual(isSafeToClean(envPath), false, '.env path must be blocked');
    assert.strictEqual(isSafeToClean(gitPath), false, '.git path must be blocked');
  }
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Vague Triggers

Medium
Confidence
93% confidence
Finding
The example trigger phrase "Clean up my disk" is broad, natural language that overlaps with common everyday requests. In an agent environment, this can cause the skill to activate too readily and initiate destructive or privacy-impacting cleanup actions, especially since the default example invokes the cleanup command without --dry-run.

Intent-Code Divergence

Medium
Confidence
97% confidence
Finding
The header states 'ZERO external dependencies. Must run on raw Node.js,' which implies the skill relies only on built-in Node.js functionality. However, later code uses child_process.execSync to invoke external binaries such as docker, npm, yarn, apt-get, brew, pip, and others, so the documentation actively contradicts the implementation.