Back to skill

Security audit

Weekly Planner

Security checks for vulnerabilities and agentic risk

Overview

The planner is coherent, but it needs review because one setup option can delete any chosen folder and optional calendar sync depends on an unpinned third-party tool.

Install only if you are comfortable with a local planner that writes files in your workspace. Run init without --force unless you have verified the resolved target is only a disposable planner directory. Prefer ICS export unless you trust the gogcli installation source; if using Google Calendar sync, use a dedicated calendar, review dry-run output, and keep calendar.write_enabled false until you deliberately apply changes.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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
Findings (2)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/init_planner.py:27
Finding
Arbitrary Recursive Directory Deletion Through the Force Option<![CDATA[ ## Vulnerability Details **File Location**: `scripts/init_planner.py`, lines 27–33 and 44–60 **Vulnerability Type**: Unrestricted recursive deletion of a user-selected filesystem path **Risk Level**: High ### Vulnerable Code ```python ap.add_argument( "--target", default="./planner", help="Target directory to create (default: ./planner)", ) ap.add_argument( "--force", action="store_true", help="Overwrite the target directory if it already exists (DANGEROUS).", ) ``` ```python target_dir = Path(args.target).expanduser().resolve() if target_dir.exists(): if any(target_dir.iterdir()): if not args.force: die( "Refusing to overwrite existing non-empty directory:\n" f" {target_dir}\n\n" "If you really want to replace it, re-run with --force (this will delete it first)." ) shutil.rmtree(target_dir) else: # copytree() requires the destination to not exist shutil.rmtree(target_dir) shutil.copytree(template_dir, target_dir) ``` ### Technical Analysis The `--target` argument accepts an arbitrary path, expands user-directory syntax, and resolves it to an absolute path. If `--force` is supplied, the script passes that path directly to `shutil.rmtree()` without enforcing any filesystem boundary. The script does not: - Require the target to be inside the current workspace. - Reject dangerous locations such as the user's home directory, repository root, or another unrelated data directory. - Require a planner-specific sentinel file before deletion. - Request interactive confirmation showing the resolved path. - Create a backup before recursively deleting the target. - Verify that the target is the expected planner directory. The warning in the argument description does not prevent accidental or attacker-influenced invocation. This is an unsafe destructive-operation design rather than a privilege-escalation flaw: deletion ...[truncated 1266 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Implement layered safeguards around destructive replacement: 1. Restrict the resolved target to an explicitly approved workspace root using `Path.relative_to()` or an equivalent containment check. 2. Reject dangerous targets, including: - Filesystem root. - The user's home directory. - The current workspace root. - The Skill installation directory. - Any directory outside the approved workspace. 3. Require a planner-specific sentinel file before replacing an existing non-empty directory. 4. Separate initialization from replacement. Prefer refusing all non-empty targets and provide a dedicated migration or reset command. 5. If forced replacement remains necessary, require interactive confirmation that reproduces the fully resolved path. 6. Move the existing planner to a timestamped backup rather than deleting it immediately. 7. Reject symlink targets and verify relevant path components before deletion. 8. Consider requiring both a narrowly named flag such as `--replace-existing-planner` and a confirmation token rather than a generic `--force`. Example containment control: ```python workspace = Path.cwd().resolve() target_dir = Path(args.target).expanduser().resolve() try: target_dir.relative_to(workspace) except ValueError: die(f"Refusing target outside workspace: {target_dir}") dangerous = {Path("/").resolve(), Path.home().resolve(), workspace} if target_dir in dangerous: die(f"Refusing dangerous target: {target_dir}") sentinel = target_dir / "config.toml" if target_dir.exists() and any(target_dir.iterdir()) and not sentinel.is_file(): die("Refusing to replace a directory that is not an existing planner.") ``` ]]>

T08 · Insecure Dependencies

Warning
Location
SKILL.md:6
Finding
Unpinned Third-Party Calendar CLI Installation Creates a Supply-Chain Risk<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md`, line 6; `references/CALENDAR_SYNC.md`, lines 19–26 **Vulnerability Type**: Installation of an unpinned external dependency from a third-party Homebrew tap **Risk Level**: Medium ### Vulnerable Instructions From `SKILL.md`: ```yaml metadata: {"version":"0.2.0","tags":["planner","weekly-review","time-blocking","productivity"],"openclaw":{"emoji":"🗓️","install":[{"id":"brew-python","kind":"brew","formula":"python","bins":["python3"],"label":"Install Python 3 (brew)"},{"id":"brew-gogcli","kind":"brew","formula":"steipete/tap/gogcli","bins":["gog"],"label":"Install gogcli (brew)"}]}} ``` From `references/CALENDAR_SYNC.md`: ```markdown ### Requirements - Python 3.11+ - The `gog` CLI (`steipete/gogcli`) available on `PATH` - A dedicated Google Calendar ID configured in `planner/config.toml` Install (macOS/Homebrew): ```bash brew install steipete/tap/gogcli gog --version ``` ``` ### Technical Analysis The Skill recommends or declares installation of `gogcli` from the external Homebrew tap `steipete/tap` without pinning a reviewed version, commit, bottle digest, or artifact checksum. Consequently, the dependency retrieved at installation time can differ from the component that existed when the Skill was audited. This dependency is security-sensitive because `assets/planner_template/scripts/sync_week.py` locates `gog` through `PATH` and invokes it for authenticated Google Calendar queries, event creation, updates, and deletion. A compromised or unexpectedly modified formula or binary would execute with the local privileges of the agent process and could access whatever Google authorization is available to `gog`. No evidence was found that the currently named upstream package is malicious. The finding concerns the absence of supply-chain integrity controls for a privileged optional dependency. ### Attack Path 1. The third-party tap, its maintainer account, its distribution infrastructure, or ...[truncated 1430 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin `gogcli` to a reviewed release rather than installing an unconstrained current version. 2. Record and verify a cryptographic digest or trusted package signature for the expected artifact. 3. Pin the Homebrew tap or formula to a reviewed commit where operationally possible. 4. Document the exact reviewed package source, version, expected publisher, and integrity-verification procedure. 5. Require users to inspect the resolved formula and verify the installed executable before authentication. 6. Grant the narrowest available Google OAuth scopes and use a dedicated planner calendar and account where practical. 7. Keep the ICS export as the default path and require explicit opt-in for the external authenticated integration. 8. Before execution, verify that the resolved `gog` path is an expected trusted installation location and optionally validate its version and binary digest. 9. Treat any dependency update as requiring renewed security review before calendar synchronization is enabled. ]]>
Vulnerability Patterns
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (8)

Self-Modification

High
Category
Rogue Agent
Content
out_path = Path(args.out).expanduser().resolve() if args.out else (planner_dir / "weeks" / f"{week_id}.toml")

    if out_path.exists():
        raise SystemExit(f"Refusing to overwrite existing file: {out_path}")

    if not runbook_path.exists():
        raise SystemExit(f"Runbook not found: {runbook_path}")
Confidence
85% confidence
Finding
Skill modifies its own code, configuration, or behavior at runtime. Self-modification enables an agent to escalate privileges, disable safety constraints, or install persistent backdoors.

Self-Modification

High
Category
Rogue Agent
Content
out_path = Path(args.out).expanduser().resolve() if args.out else (planner_dir / "weeks" / f"{week_id}.toml")

    if out_path.exists():
        raise SystemExit(f"Refusing to overwrite existing file: {out_path}")

    if not runbook_path.exists():
        raise SystemExit(f"Runbook not found: {runbook_path}")
Confidence
85% confidence
Finding
Skill modifies its own code, configuration, or behavior at runtime. Self-modification enables an agent to escalate privileges, disable safety constraints, or install persistent backdoors.

Vague Triggers

Medium
Confidence
97% confidence
Finding
The trigger says the mode applies when 'building, writing, coding, analysis, or any task that benefits from uninterrupted focus,' which overlaps with many common activities and adds an open-ended catch-all condition. The file does not provide specific scope limits, explicit trigger phrases, or negative examples to clarify when this mode should not activate.

Vague Triggers

Medium
Confidence
95% confidence
Finding
The trigger description uses general states like "scattered," "doing admin/errands," and "start-of-day organisation," which are common situations rather than specific invocation cues. The file does not provide constraints, explicit trigger phrases, or negative examples to clarify when this mode should or should not activate.

Vague Triggers

Medium
Confidence
96% confidence
Finding
The phrase "When: spending intentional time with partner/family/friends" is a high-level life context rather than a specific invocation trigger. It lacks clear boundaries, examples, or exclusions, so it could overlap with common daily interactions and make it unclear when this mode should activate versus not activate.

Vague Triggers

Medium
Confidence
95% confidence
Finding
The trigger list uses subjective, everyday states like "stuck" and broad emotional conditions such as "dysregulated" or "spiralling" without defining scope or exclusion conditions. Because the file provides no negative examples or contextual constraints, the skill could activate in situations far beyond the intended reset use case.

Vague Triggers

Medium
Confidence
94% confidence
Finding
The phrase "end of workday OR late evening" is subjective and varies by user, schedule, and context. The file does not define concrete boundaries or exclusions, so the mode could be invoked unintentionally in common situations.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def sh(cmd: list[str]) -> str:
    try:
        return subprocess.check_output(cmd, text=True)
    except subprocess.CalledProcessError as e:
        # Preserve any partial output for debugging.
        if e.output:
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Static analysis

No suspicious patterns detected.