Back to skill

Security audit

Workspace Cleaner

Security checks for vulnerabilities and agentic risk

Overview

This workspace cleanup skill is mostly transparent, but it can move valuable workspace files or whole repositories to trash under automatic or under-scoped cleanup rules.

Review this skill carefully before installing. Use preview mode only unless you have checked every target, avoid unattended --execute runs, do not allow custom configs from untrusted sources, and treat duplicate repository cleanup as high risk until it verifies clean working trees, matching commits, and no local-only data.

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

Error
Location
scripts/cleanup.py:148
Finding
Repository Cleanup Can Discard Unique Local Work Based Only on a Matching Remote URL## Vulnerability Details **File Location**: `scripts/cleanup.py`, lines 103–109, 148–155, and 309–313 **Vulnerability Type**: Unsafe destructive repository classification **Risk Level**: High ### Vulnerable Code ```python project_remotes = {} projects_dir = workspace / "projects" if projects_dir.exists(): for pdir in projects_dir.iterdir(): if pdir.is_dir() and (pdir / ".git").exists(): remote = get_git_remote(pdir) if remote: project_remotes[remote] = pdir.name ``` ```python # Duplicate repos elif (item / ".git").exists(): remote = get_git_remote(item) if remote and remote in project_remotes: target = { "path": item, "reason": f"duplicate of projects/{project_remotes[remote]}" } ``` ```python for t in targets: if trash_item(t["path"]): success += 1 if not args.quiet: print(f" Trashed: {t['path'].name}") ``` ### Technical Analysis The implementation treats two Git repositories as duplicates solely because they have the same `origin` URL. A shared remote does not establish that two working trees contain identical data. A root-level repository may contain: - Uncommitted modifications - Untracked or ignored files - Local-only branches - Commits that have not been pushed - A different checked-out revision - Local build artifacts or configuration not present in the other repository The cleaner does not inspect `git status`, compare HEAD revisions, identify untracked files, check branch divergence, or verify file-level equivalence. Once a repository is classified as a duplicate, execution mode passes the entire directory to `trash_item()`. Although the normal workflow defaults to preview mode, documented automation examples invoke `--execute`. The destructive classification can therefore operate without item-by-item confirmation. ### Attack P ...[truncated 1150 chars]
Remediation
## Remediation Suggestions 1. Make repository detection report-only by default, even when general cleanup runs with `--execute`. 2. Require explicit per-repository confirmation or a separate high-risk command-line option before moving any Git repository. 3. Before declaring a repository redundant, verify all of the following: - The working tree and index are clean. - No untracked or ignored user data exists. - HEAD commits are identical. - No local-only branches or tags exist. - No commits are ahead of every configured remote. - Relevant file content is equivalent. 4. Prefer Git-aware archival or a generated review report over automatic removal. 5. Generate a cleanup manifest containing the source path, trash destination, repository state, HEAD commit, and timestamp. 6. Abort repository cleanup if any Git inspection command fails or returns ambiguous results. 7. Add automated tests covering uncommitted changes, untracked files, divergent branches, local-only commits, missing remotes, and repositories with identical remotes but different content.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/cleanup.py:45
Finding
Custom Configuration Can Replace Mandatory Workspace Protection Lists## Vulnerability Details **File Location**: `scripts/cleanup.py`, lines 45–49 and 116–119 **Vulnerability Type**: Fail-open safety configuration **Risk Level**: Medium ### Vulnerable Code ```python def load_config(config_path: Optional[Path]) -> dict: """Load configuration from file or use defaults.""" if config_path and config_path.exists(): with open(config_path) as f: user_config = json.load(f) # Merge with defaults config = DEFAULT_CONFIG.copy() config.update(user_config) return config return DEFAULT_CONFIG.copy() ``` ```python # Skip protected if item.name in config["protected_dirs"]: continue if item.name in config["protected_files"]: continue ``` ### Technical Analysis `dict.update()` replaces complete configuration values. Consequently, user-supplied `protected_dirs` and `protected_files` arrays replace the default protection arrays instead of extending them. The runtime protection check relies exclusively on the resulting configuration. There is no separate immutable set of mandatory protected files and directories. This behavior contradicts the documentation stating that protected locations are never deleted regardless of settings. A configuration can remove a protected file from `protected_files` and add its extension to `temp_extensions`. For example, omitting `MEMORY.md` from the protection list while adding `.md` as a temporary extension would make that root-level file eligible for cleanup, subject to the age filters. Similar configuration changes can weaken directory protections where a directory also matches one of the implemented cleanup branches. The configuration loader also performs no schema or type validation, no rejection of unsafe protection removal, and no warning when mandatory safeguards are replaced. ### Attack Path 1. A user or automation process invokes the cleaner with `--conf ...[truncated 1045 chars]
Remediation
## Remediation Suggestions 1. Define immutable mandatory protection sets separately from customizable defaults. 2. Union user-provided protection entries with mandatory entries rather than replacing them: ```python config["protected_dirs"] = sorted( MANDATORY_PROTECTED_DIRS | set(user_config.get("protected_dirs", [])) ) config["protected_files"] = sorted( MANDATORY_PROTECTED_FILES | set(user_config.get("protected_files", [])) ) ``` 3. Reject configurations that attempt to remove mandatory protections. 4. Validate the complete JSON schema, including expected object types, arrays of strings, supported keys, and pattern syntax. 5. Detect and reject cleanup patterns that directly match mandatory protected locations. 6. Resolve and validate the workspace path before scanning, and enforce protection against resolved paths rather than names alone. 7. Emit a prominent warning and refuse execution if configuration validation fails. 8. Add tests proving that all mandatory files and directories remain protected under empty, partial, malformed, and adversarial custom configurations. 9. Update the documentation to accurately distinguish immutable safeguards from user-configurable exclusions.
Vulnerability Patterns
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • 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
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (7)

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The documentation explicitly suggests unattended or semi-automatic cleanup in HEARTBEAT automation and weekly maintenance examples, including auto-cleaning files based on age and size. Even though the skill describes safeguards such as trash usage and preview mode, encouraging automated deletion without a strong warning about false positives, path/config mistakes, or review requirements can lead to accidental loss of important workspace data.

Intent-Code Divergence

Medium
Confidence
77% confidence
Finding
The reference states an absolute safety guarantee for protected locations and frames the tool as 'safe by default'. However, the same document later promotes unattended execution via heartbeat, pre-commit hook, and cron examples using `--execute`, which weakens the earlier safety framing and creates contradictory operator expectations about when deletion occurs. This is an intent/documentation divergence rather than a code-level permission issue.

Intent-Code Divergence

Medium
Confidence
88% confidence
Finding
The document marks some targets as requiring human review, then later presents unattended `--execute` automation as normal usage. That mismatch can lead operators to deploy destructive cleanup flows that remove images, virtual environments, or other borderline artifacts without the intended review step, increasing the chance of accidental data loss.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The HEARTBEAT, pre-commit, and cron examples all normalize unattended destructive execution without prominent warnings about operational risk, recovery limits, or false-positive cleanup. In an agent or automation context, this increases the likelihood of silent, repeated deletion of user files or environment state, especially when cleanup criteria evolve over time.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def get_git_remote(repo_path: Path) -> Optional[str]:
    """Get git remote URL for a repository."""
    try:
        result = subprocess.run(
            ["git", "remote", "get-url", "origin"],
            cwd=repo_path,
            capture_output=True,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
"""Move item to trash. Returns True on success."""
    try:
        # Try 'trash' command (macOS)
        result = subprocess.run(
            ["trash", str(path)],
            capture_output=True
        )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
return True
        
        # Try 'trash-put' (Linux trash-cli)
        result = subprocess.run(
            ["trash-put", str(path)],
            capture_output=True
        )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Static analysis

No suspicious patterns detected.