Back to skill

Security audit

Workspace Org

Security checks for vulnerabilities and agentic risk

Overview

This skill has a coherent workspace-organization purpose, but its fix script can move broad workspace content and can follow symlinks outside the intended workspace boundary.

Review this skill before installing. Use only the dry-run audit first, inspect every proposed move, and avoid running apply.py --execute on untrusted or symlink-heavy workspaces until the script adds containment checks, explicit confirmation, exclusions, and rollback/journaling. The behavior appears unsafe rather than deceptive; I found no exfiltration, remote code loading, or persistence mechanism.

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/apply.py:53
Finding

Workspace migration follows symlinked directories and can move files outside the workspace

Content
View full analysis

Vulnerability Details

File Location: scripts/apply.py, lines 53–78 and 177–180
Vulnerability Type: Unrestricted filesystem migration through symlink traversal
Risk Level: High

Vulnerable Code

python
# 2. Flatten old _tmp/_notes/... directories into files/{new}
for old_dir_name, new_sub in OLD_TO_NEW.items():
    old_dir = workspace / old_dir_name
    if not old_dir.is_dir():
        continue
    for child in sorted(old_dir.iterdir()):
        target = files_dir / new_sub / child.name
        if not target.exists():
            actions.append({
                "type": "move", "source": child, "target": target,
                "dest": f"{FILES_DIR}/{new_sub}",
                "reason": f"merge from {old_dir_name}",
            })
    # Remove old dir after emptying it
    actions.append({
        "type": "remove_dir", "path": old_dir, "name": old_dir_name,
        "reason": "replaced by files/",
    })

The planned move is subsequently executed without validating either endpoint:

python
elif a["type"] == "move":
    rel = a["source"].name
    if dry_run:
        print(f"  [MOVE]    {rel:35s} → {a['dest']}/  ({a['reason']})")
    else:
        a["target"].parent.mkdir(parents=True, exist_ok=True)
        a["source"].rename(a["target"])
        print(f"  ✅  Moved   {rel:35s} → {a['dest']}/")

Technical Analysis

Path.is_dir() and Path.iterdir() follow directory symlinks. The script does not reject symlinks or verify that resolved source and destination paths remain beneath the selected workspace.

Consequently, a legacy directory such as _tmp, _notes, or _archive can be a symlink to a directory outside the workspace. The planner will enumerate the external directory as though it were local workspace content. During execution, Path.rename() then moves the external directory's children to the calculated destination.

The destination tree is similarly unprotected. If files or one of its category director ...[truncated 1729 chars]

Remediation
View remediation

Remediation Suggestions

  1. Reject symlinks for the workspace, files, legacy directories, migration sources, destination parents, and destination paths.
  2. Resolve each source and destination and enforce workspace containment before planning and immediately before mutation:
python
def require_within_workspace(path: Path, workspace: Path) -> Path:
    root = workspace.resolve(strict=True)
    resolved = path.resolve(strict=True)
    if resolved != root and root not in resolved.parents:
        raise ValueError(f"Path escapes workspace: {path}")
    return resolved
  1. Use Path.lstat() or Path.is_symlink() before is_dir() and iterdir() so that directory symlinks are detected rather than followed.
  2. Validate every component of destination paths because checking only the final lexical path does not detect symlinked parents.
  3. Repeat validation directly before rename() to reduce time-of-check/time-of-use race exposure.
  4. Where supported, use descriptor-relative filesystem operations with no-follow semantics instead of path-based checks alone.
  5. Abort execution on any boundary violation and report the unsafe item rather than silently skipping it.
  6. Add automated tests covering symlinked legacy directories, symlinked files directories, nested symlink parents, broken symlinks, and paths outside the workspace.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/apply.py:80
Finding

Broad automatic classification can relocate unrecognized operational workspace content

Content
View full analysis

Vulnerability Details

File Location: scripts/apply.py, lines 80–113 and 121–153
Vulnerability Type: Unsafe destructive workspace migration
Risk Level: Medium

Vulnerable Code

python
# 3. Migrate unknown directories
for item in sorted(workspace.iterdir()):
    if item.name.startswith(".") or item.name in PROTECTED_DIRS | PROTECTED_FILES:
        continue
    if item.is_dir():
        dest = classify_dir(item, mode, subdirs)
        if dest:
            target = files_dir / dest / item.name
            if not target.exists():
                actions.append({
                    "type": "move", "source": item, "target": target,
                    "dest": f"{FILES_DIR}/{dest}",
                    "reason": classify_dir_reason(item),
                })

# 4. Migrate unknown files
for item in sorted(workspace.iterdir()):
    if item.name.startswith(".") or item.name in PROTECTED_DIRS | PROTECTED_FILES:
        continue
    if item.is_dir():
        continue
    dest = classify_file(item)
    if dest:
        target = files_dir / dest / item.name
        if not target.exists():
            actions.append({
                "type": "move", "source": item, "target": target,
                "dest": f"{FILES_DIR}/{dest}",
                "reason": classify_file_reason(item),
            })

Unknown directories default to archival migration:

python
def classify_dir(path: Path, mode: str, subdirs: list[str]) -> str | None:
    name = path.name
    # Never move directories that are active agent workspaces
    if (path / "AGENTS.md").exists() or (path / "SOUL.md").exists():
        return None
    if mode == "root":
        if name in ROOT_AGENT_DIRS or name in ("runtime",):
            return None
        if name.startswith("expert-"):
            return "experts"
        return "archive"
    return "archive"

Unknown files also default to archival migration:

python
def classify_file(path: Path) -> str | None:
    name = path.
...[truncated 2965 chars]
Remediation
View remediation

Remediation Suggestions

  1. Replace default-move behavior with default-deny behavior: ambiguous items should be reported but not automatically relocated.
  2. Require explicit approval for the complete migration plan before executing any move.
  3. Add repeatable --include and --exclude options and support a workspace-local policy file.
  4. Expand protection beyond a fixed filename list by detecting relevant project manifests and operational directory markers.
  5. Require an explicit flag for broad archival behavior, separate from directory creation and well-defined legacy migration.
  6. Write a machine-readable migration journal containing original and destination paths.
  7. Provide a rollback command that validates the journal and restores successfully moved items.
  8. Handle destination collisions explicitly rather than silently omitting affected items.
  9. Prefer transactional behavior: validate the entire plan before the first move and stop or roll back if any operation fails.
  10. Add tests using representative Python, JavaScript, container, deployment, and multi-agent workspaces to ensure active project assets are not moved automatically.
Vulnerability Patterns
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • 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 (3)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding

Without declared permissions the skill's intent is opaque and cannot be validated.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The manifest description includes broad trigger language such as workspace setup, file placement decisions, cleanup, and compliance checks, which could cause the skill to activate in routine conversations unrelated to an intentional reorganization request. Over-broad activation increases the chance an agent suggests or invokes filesystem-auditing and file-moving behavior in contexts where the user did not clearly ask for it.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The skill advertises a fix command, python3 {{SKILL_DIR}}/scripts/apply.py --execute, that migrates existing files into new locations, but the surrounding documentation does not clearly warn that this changes filesystem state and may disrupt workflows, references, or automation expecting files at current paths. In an agent context, a command framed as a routine fix can be executed too casually, causing unintended file moves and possible data handling mistakes.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.