Back to skill

Security audit

soul-guardian

Security checks for vulnerabilities and agentic risk

Overview

This integrity tool is coherent, but it needs review because weak path handling and setup snippets can let it affect files outside the intended workspace or run unintended shell commands.

Review the policy file before use and reject any absolute paths, .. traversal, or unexpected targets. Keep the state directory private, prefer a restrictive umask or chmod 700/600, start with --no-restore until baselines are trusted, and avoid launchd --install unless you intentionally want recurring background execution. Do not paste generated shell snippets from workspaces or state paths containing quotes, semicolons, newlines, or other shell metacharacters.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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 (4)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/soul_guardian.py:199
Finding

Policy Paths Can Escape the Workspace and Access Arbitrary Files

Content
View full analysis
list[dict[str, str]]: """Return list of effective targets to consider. For entries with {path, mode}: direct file. For entries with {pattern, mode}: expands via globbing relative to root. Note: We keep it simple and only expand within workspace root. """ targets: list[dict[str, str]] = [] entries = policy.get("targets", []) for ent in entries: mode = ent.get("mode") if mode not in ("restore", "alert", "ignore"): continue if "path" in ent: p = Path(ent["path"]) targets.append({"path": p.as_posix(), "mode": mode}) continue pat = ent.get("pattern") if not pat: continue for match in root.glob(pat): if match.is_dir(): continue rel = relpath_str(match, root) targets.append({"path": rel, "mode": mode}) dedup: dict[str, str] = {} for t in targets: dedup[t["path"]] = t["mode"] return [{"path": p, "mode": m} for p, m in sorted(dedup.items())] def approved_snapshot_path(state: GuardianState, rel_path: str) -> Path: # Preserve relative structure under approved/. return state.approved_dir / Path(rel_path) ``` The resulting path is subsequently used without a workspace containment check: ```python relp = t["path"] mode = t["mode"] if mode == "ignore": continue abs_path = WORKSPACE_ROOT / relp if not abs_path.exists(): continue refuse_symlink(abs_path) ``` The same construction is used during drift detection and restoration: ```python def detect_drift_for( state: GuardianState, baselines: dict[str, Any], relp: ...[truncated 2962 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/soul_guardian.py:90
Finding

Sensitive Guardian State Is Created Without Enforced Restrictive Permissions

Content
View full analysis
None: path.mkdir(parents=True, exist_ok=True) def atomic_write_bytes(path: Path, data: bytes) -> None: ensure_dir(path.parent) tmp = path.with_name(path.name + ".tmp") with tmp.open("wb") as f: f.write(data) f.flush() os.fsync(f.fileno()) os.replace(tmp, path) ``` The onboarding helper uses the same ambient-permission approach: ```python def ensure_dir(p: Path) -> None: p.mkdir(parents=True, exist_ok=True) ``` ```python if args.state_dir: external = Path(args.state_dir).expanduser() else: external = ( Path("~/.openclaw/soul-guardian").expanduser() / args.agent_id ) ensure_dir(external) ``` Sensitive workspace content is then copied into the state directory: ```python # Snapshot. snap = approved_snapshot_path(state, relp) ensure_dir(snap.parent) atomic_write_bytes(snap, read_bytes(abs_path)) ``` Drifted content is also retained: ```python quarantine_path = ( state.quarantine_dir / f"{safe_patch_tag(relp.replace('/', '_'))}.{ts}.quarantine" ) atomic_write_bytes(quarantine_path, read_bytes(abs_path)) ``` ### Technical Analysis The state directory contains approved copies of protected instruction files, patches showing changed content, quarantined versions of drifted files, policy information, hashes, and audit metadata. Directory and file creation relies entirely on the process umask. The code does not explicitly establish `0700` permissions for directories or `0600` permissions for files. Under a common `022` umask, newly created directories may be mode `0755` and files may be mode `0644`, making them readable by other local users. The documentation recommends manually ...[truncated 1428 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/onboard_state_dir.py:107
Finding

Generated Monitoring Commands Permit Shell Injection Through Crafted Paths

Content
View full analysis
None: """Output instructions and HEARTBEAT.md snippet for easy monitoring setup.""" heartbeat_snippet = """ ## Soul Guardian Check - Run `cd {workspace} && python3 skills/soul-guardian/scripts/soul_guardian.py check --actor heartbeat --output-format alert` - If any output is produced, relay it to the user immediately as a security alert - This checks for unauthorized changes to SOUL.md, AGENTS.md, and other protected files """.format(workspace=WORKSPACE_ROOT) ``` ### Technical Analysis `WORKSPACE_ROOT` is derived from the current working directory, while `external` may come from the user-supplied `--state-dir` argument. These values are embedded into commands intended to be copied into a shell, cron specification, or persistent `HEARTBEAT.md` instruction. The onboarding helper surrounds paths with single quotes but does ...[truncated 1960 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/soul_guardian.py:311
Finding

Concurrent Audit Writers Can Corrupt the Tamper-Evident Hash Chain

Content
View full analysis
str: if not state.audit_path.exists(): return CHAIN_GENESIS # Read last non-empty line without loading huge files. with state.audit_path.open("rb") as f: f.seek(0, os.SEEK_END) size = f.tell() if size == 0: return CHAIN_GENESIS block = 65536 start = max(0, size - block) f.seek(start) data = f.read() lines = [ln for ln in data.splitlines() if ln.strip()] if not lines: return CHAIN_GENESIS last = lines[-1] try: rec = json.loads(last.decode("utf-8")) return rec.get("chain", {}).get("hash") or CHAIN_GENESIS except Exception: return CHAIN_GENESIS def append_audit(state: GuardianState, entry: dict[str, Any]) -> None: state.ensure_dirs() if _audit_needs_upgrade(state): _rotate_legacy_audit(state) prev = _last_audit_hash(state) entry_wo_chain = dict(entry) entry_wo_chain.pop("chain", None) payload = prev + "\n" + _canonical_json(entry_wo_chain) cur = sha256_text(payload) record = dict(entry_wo_chain) record["chain"] = {"prev": prev, "hash": cur} with state.audit_path.open("a", encoding="utf-8") as f: f.write(json.dumps(record, ensure_ascii=False) + "\n") ``` ### Technical Analysis The integrity of each audit record depends on the hash of the immediately preceding record. However, retrieving the predecessor hash and appending the new record are separate, unlocked operations. Two guardian processes can execute the following sequence: 1. Process A reads predecessor hash `H`. 2. Process B reads the same predecessor hash `H`. 3. Process A appends a record whose `chain.prev` is `H`. 4. Process B appends a ...[truncated 1985 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
  • Rogue AgentSelf-Modification, Session Persistence
Findings (91)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

The skill is presented as an integrity guard, but the analyzed behavior includes launchd/LaunchAgent management, subprocess invocation, plist file writes, and migration validation logic unrelated to core drift detection. In a security-sensitive tool, that discrepancy increases operational risk because persistence and host-level configuration changes may be normalized or overlooked during review.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
92% confidence
Finding

The skill is presented as an integrity guard, but the analyzed behavior includes launchd/LaunchAgent management, subprocess invocation, plist file writes, and migration validation logic unrelated to core drift detection. In a security-sensitive tool, that discrepancy increases operational risk because persistence and host-level configuration changes may be normalized or overlooked during review.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

The skill is presented as an integrity guard, but the analyzed behavior includes launchd/LaunchAgent management, subprocess invocation, plist file writes, and migration validation logic unrelated to core drift detection. In a security-sensitive tool, that discrepancy increases operational risk because persistence and host-level configuration changes may be normalized or overlooked during review.

Content

No source excerpt is available for this finding.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 28)May include surrounding context.

md
For standalone installs, verify the signed release manifest before trusting `SKILL.md`, `skill.json`, or the archive. The `skill.json` file is the package metad

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 97)May include surrounding context.

md
For standalone installs, verify the signed release manifest before trusting `SKILL.md`, `skill.json`, or the archive. The `skill.json` file is the package metad

External Script Fetching

High
Category
Supply Chain
Confidence
90% confidence
Finding

Remote code is downloaded and executed. This bypasses code review and could introduce malicious code.

Content

Scanner excerpt · SKILL.md (reported line 44)May include surrounding context.

md
RELEASE_PUBKEY_SHA256="711424e4535f84093fefb024cd1ca4ec87439e53907b305b79a631d5befba9c8"

curl -fsSL "$BASE/checksums.json" -o "$TMP_DIR/checksums.json"
curl -fsSL "$BASE/checksums.sig" -o "$TMP_DIR/checksums.sig"
curl -fsSL "$BASE/signing-public.pem" -o "$TMP_DIR/signing-public.pem"
curl -fsSL "$BASE/$ZIP_NAME" -o "$TMP_DIR/$ZIP_NAME"

Direct Prompt Extraction

High
Category
System Prompt Leakage
Confidence
85% confidence
Finding

Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Content

Scanner excerpt · scripts/soul_guardian.py (reported line 711)May include surrounding context.

python
def enable_monitoring_cmd(state: GuardianState) -> None:
    """Output instructions and HEARTBEAT.md snippet for easy monitoring setup."""
    
    heartbeat_snippet = """
## Soul Guardian Check

Direct Prompt Extraction

High
Category
System Prompt Leakage
Confidence
85% confidence
Finding

Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Content

Scanner excerpt · scripts/soul_guardian.py (reported line 971)May include surrounding context.

python
add_common(sp_watch)
    sp_watch.add_argument("--interval", type=int, default=30, help="Check interval in seconds (default: 30).")
    
    sub.add_parser("enable-monitoring", help="Show instructions for enabling automatic monitoring and alerts.")

    return p.parse_args(argv)

Env Variable Harvesting

High
Category
Data Exfiltration
Confidence
60% confidence
Finding

Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Content

Scanner excerpt · scripts/test_install_launchd_plist.py (reported line 36)May include surrounding context.

python
def run_case(home_dir: Path, agent_id: str) -> subprocess.CompletedProcess:
    env = os.environ.copy()
    env["HOME"] = str(home_dir)
    plist_path = home_dir / "LaunchAgents" / f"{agent_id}.plist"
    cmd = [

Env Variable Harvesting

High
Category
Data Exfiltration
Confidence
60% confidence
Finding

Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Content

Scanner excerpt · scripts/test_install_launchd_plist.py (reported line 141)May include surrounding context.

python
agent_id,
                "--force",
            ],
            {**os.environ, "HOME": str(home_dir)},
        )
        must_ok(cp)
        assert_contains(cp.stdout, legacy_label, "legacy label dry-run note")

Session Persistence

Medium
Category
Rogue Agent
Confidence
80% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · CHANGELOG.md (reported line 17)May include surrounding context.

md
### Added

- Regression coverage for launchd label migration so the installer documents and cleans up the previous Clawdbot-era label before starting the new default label.

### Changed

Session Persistence

Medium
Category
Rogue Agent
Confidence
80% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · CHANGELOG.md (reported line 21)May include surrounding context.

md
### Changed

- `scripts/install_launchd_plist.py` now documents the legacy launchd label/plist in dry-run output and attempts a best-effort disable/bootout of `com.clawdbot.soul-guardian.<agentId>` before installing `com.openclaw.soul-guardian.<agentId>`.
- The `--label` help now explains that non-legacy labels trigger legacy-job cleanup, while explicitly selecting the legacy label skips that migration path.

### Security

Session Persistence

Medium
Category
Rogue Agent
Confidence
80% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · CHANGELOG.md (reported line 32)May include surrounding context.

md
### Added

- Regression coverage for launchd state-directory selection so existing legacy installs keep using their current guardian state unless the operator explicitly chooses a new location.

### Changed

Session Persistence

Medium
Category
Rogue Agent
Confidence
80% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · CHANGELOG.md (reported line 37)May include surrounding context.

md
### Changed

- `scripts/install_launchd_plist.py` now reuses `~/.clawdbot/soul-guardian/<agentId>/` when that legacy state directory already exists and otherwise keeps the new `~/.openclaw/...` default.
- The launchd installer now prints an explicit migration warning with the `--state-dir` value to use when switching an existing install to the new OpenClaw path.

### Security

Session Persistence

Medium
Category
Rogue Agent
Confidence
80% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · skill.json (reported line 43)May include surrounding context.

json
### Changed

- `scripts/install_launchd_plist.py` now reuses `~/.clawdbot/soul-guardian/<agentId>/` when that legacy state directory already exists and otherwise keeps the new `~/.openclaw/...` default.
- The launchd installer now prints an explicit migration warning with the `--state-dir` value to use when switching an existing install to the new OpenClaw path.

### Security

Session Persistence

Medium
Category
Rogue Agent
Confidence
82% confidence
Finding

The README explicitly recommends creating a persistent state directory outside the workspace and storing approved snapshots, baselines, patches, quarantine copies, and audit data there. While this is presented as resilience, it creates durable storage of potentially sensitive prompt, memory, and instruction content, increasing exposure and forensic persistence if the host or home directory is later accessed by an attacker.

Content

Scanner excerpt · README.md (reported line 71)May include surrounding context.

md
### First run / Initialize baselines (recommended)

For resilience, create your guardian **state directory outside** the workspace first, then initialize baselines.

1) Onboard an external state dir (creates policy, copies any existing state, prints paths/snippets):

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
80% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · README.md (reported line 192)May include surrounding context.

md
Notes:
- `<agentId>` should be **stable and unique per workspace** (don’t point multiple workspaces at the same state dir).
- WARNING: `--move` deletes the old in-workspace state dir after copying.
- The external state dir can contain **approved snapshots, patches, and quarantined copies** of sensitive prompt/instruction/memory files. Keep permissions restrictive (e.g., `chmod 700 <dir>`; `chmod go-rwx <dir>`).

Then include `--state-dir` in all commands (run from the workspace root), e.g.:

Session Persistence

Medium
Category
Rogue Agent
Confidence
90% confidence
Finding

This section documents generating and optionally installing a LaunchAgent plist for recurring execution, which establishes persistence on macOS. Even though the feature is described as optional and administrative, scheduled background execution is a persistence mechanism and could be abused to continuously monitor, restore, or preserve agent-controlled files without frequent user attention.

Content

Scanner excerpt · README.md (reported line 229)May include surrounding context.

Because soul_guardian.py check prints nothing on OK and prints a single-line SOUL_GUARDIAN_DRIFT ... summary on drift, this tends to be silent unless something changed.

Generate + (optionally) install a LaunchAgent plist (run from the workspace root, or pass --workspace-root):

bash
python3 skills/soul-guardian/scripts/install_launchd_plist.py \

Session Persistence

Medium
Category
Rogue Agent
Confidence
93% confidence
Finding

The --install example directly instructs installation of the LaunchAgent, moving from mere configuration generation to actual persistence. In the context of an agent skill, this is more sensitive because it enables automated recurring execution and logging on the host outside the immediate interactive session.

Content

Scanner excerpt · README.md (reported line 232)May include surrounding context.

Generate + (optionally) install a LaunchAgent plist (run from the workspace root, or pass --workspace-root):

bash
python3 skills/soul-guardian/scripts/install_launchd_plist.py \
  --state-dir ~/.openclaw/soul-guardian/<agentId> \
  --interval-seconds 600 \
  --install

Session Persistence

Medium
Category
Rogue Agent
Confidence
86% confidence
Finding

The README states that the generated plist uses the workspace root as WorkingDirectory and writes logs under the state directory, confirming an operationally persistent scheduled job with ongoing artifacts. This increases the durability of both execution and stored outputs, which can preserve sensitive data and make misuse less visible if an attacker co-opts the mechanism.

Content

Scanner excerpt · README.md (reported line 238)May include surrounding context.

--install

text

The generated plist includes `WorkingDirectory` set to your workspace root (recommended), so relative paths behave as expected.

The script writes drift output to log files under `<state-dir>/logs/`.
You can tail them with the commands it prints.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding

The skill declares executable capabilities and operational side effects but does not define an explicit tool scope such as permissions or allowed-tools. That creates a trust and review gap: consumers may install a skill that can read, write, and execute shell actions without a machine-readable restriction boundary, increasing the chance of overprivileged execution.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/install_launchd_plist.py (reported line 59)May include surrounding context.

python
def run_launchctl(args: list[str]) -> subprocess.CompletedProcess[str]:
    return subprocess.run(["/bin/launchctl", *args], check=False, text=True, capture_output=True)


def cleanup_legacy_launchd(uid: int, active_label: str, agent_id: str) -> list[str]:

Session Persistence

Medium
Category
Rogue Agent
Confidence
98% confidence
Finding

This script is explicitly designed to create a persistent launchd agent that runs automatically on an interval and at login/load. Persistence mechanisms are high-sensitivity in agent skills because they survive the initiating session and can repeatedly execute code from the workspace, increasing the blast radius if the monitored script or workspace is later modified.

Content

Scanner excerpt · scripts/install_launchd_plist.py (reported line 127)May include surrounding context.

python
ap.add_argument(
        "--label",
        default=None,
        help="launchd label (default: com.openclaw.soul-guardian.<agentId>). When using a non-legacy label, --install attempts to disable/boot out the previous com.clawdbot.soul-guardian.<agentId> job first.",
    )
    ap.add_argument(
        "--interval-seconds",

Session Persistence

Medium
Category
Rogue Agent
Confidence
88% confidence
Finding

Allowing the plist output path to be user-specified contributes to persistence installation flexibility and may permit writing launch agent definitions to arbitrary locations if the script is run in an unsafe automation context. Combined with --install, this broadens where persistent configuration can be sourced from.

Content

Scanner excerpt · scripts/install_launchd_plist.py (reported line 140)May include surrounding context.

python
ap.add_argument(
        "--out",
        default=None,
        help="Write plist to this path (default: ~/Library/LaunchAgents/<label>.plist)",
    )
    ap.add_argument("--force", action="store_true", help="Overwrite existing plist on disk.")
    ap.add_argument(

Session Persistence

Medium
Category
Rogue Agent
Confidence
98% confidence
Finding

The --install option directly enables writing and loading a launchd job, creating automatic recurring execution. In a mutable workspace context, that means later file tampering can convert a benign monitor into a persistence foothold.

Content

Scanner excerpt · scripts/install_launchd_plist.py (reported line 146)May include surrounding context.

python
ap.add_argument(
        "--install",
        action="store_true",
        help="Install+load the plist with launchctl (bootstrap). Without this flag we only write the plist.",
    )

    args = ap.parse_args(argv)

Static analysis

Detected: suspicious.dynamic_code_execution

Dynamic code execution detected.

Critical
Code
suspicious.dynamic_code_execution
Location
scripts/test_install_launchd_plist.py:66