Back to skill

Security audit

VAIBot Guard

Security checks for vulnerabilities and agentic risk

Overview

This is a coherent local guard skill, but it needs Review because its security-sensitive service can fail open, persist as a background service, mishandle local paths and redaction, and upload operational receipts when API settings are enabled.

Install only after reviewing the service configuration. Keep it bound to 127.0.0.1, require a strong VAIBOT_GUARD_TOKEN, avoid HTTP or untrusted VAIBOT_API_URL values, treat logs as sensitive, and prefer foreground/manual operation until the path validation, redaction, and high-risk tool default-allow issues are fixed.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • System PersistenceInstalls backdoors, hooks, services, or scheduled tasks that survive the run
  • 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
Findings (7)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/vaibot-guard-service.mjs:81
Finding
Path Traversal Through Client-Controlled Identifiers<![CDATA[ ## Vulnerability Details **File Location**: `scripts/vaibot-guard-service.mjs:81-82, 179-180, 789-790, 824-825, 840-841, 870-871, 877-878, 947-948, 1031-1033` **Vulnerability Type**: Unvalidated path construction and directory traversal **Risk Level**: High ### Vulnerable Code ```js function approvalPath(approvalId) { return path.join(APPROVAL_DIR, `${approvalId}.json`); } function runCtxPath(runId) { return path.join(RUNCTX_DIR, `${runId}.json`); } function loadMerkleState(sessionId) { const p = path.join(LOG_DIR, `${sessionId}.merkle.json`); // ... } function saveMerkleState(sessionId, st) { const p = path.join(LOG_DIR, `${sessionId}.merkle.json`); fs.writeFileSync(p, JSON.stringify(st, null, 2) + "\n"); } function appendLeaf(sessionId, leaf) { const p = path.join(LOG_DIR, `${sessionId}.leaves.jsonl`); fs.appendFileSync(p, stableStringify({ leaf }) + "\n"); } function loadCheckpoints(sessionId) { const cpPath = path.join(LOG_DIR, `${sessionId}.checkpoints.jsonl`); // ... } function loadLeaves(sessionId, count) { const p = path.join(LOG_DIR, `${sessionId}.leaves.jsonl`); // ... } function appendCheckpoint(sessionId, checkpoint) { const p = path.join(LOG_DIR, `${sessionId}.checkpoints.jsonl`); fs.appendFileSync(p, stableStringify(checkpoint) + "\n"); } function appendAudit(event) { const sessionId = event.sessionId || "unknown-session"; const logPath = path.join(LOG_DIR, `${sessionId}.jsonl`); const prevHashPath = path.join(LOG_DIR, `${sessionId}.prevhash`); // ... } ``` ### Technical Analysis The service accepts `sessionId`, `approvalId`, and `runId` from HTTP request bodies and directly interpolates them into filesystem paths. It does not reject path separators, `..` components, absolute-path syntax, or identifiers exceeding an expected format. `path.join()` normalizes traversal components. An identifier such as `../../target` can therefore escape `LOG_DIR`, `APPROVAL_DIR`, or `RUNCTX_DIR`. Depending o ...[truncated 1691 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Apply strict syntax validation to every externally supplied identifier: ```js function validateIdentifier(value, name) { const s = String(value || ""); if (!/^[A-Za-z0-9_-]{1,128}$/.test(s)) { throw new Error(`Invalid ${name}`); } return s; } ``` 2. Resolve every generated path and confirm it remains inside its intended root: ```js function safeChildPath(root, filename) { const rootResolved = path.resolve(root); const candidate = path.resolve(rootResolved, filename); const rel = path.relative(rootResolved, candidate); if (rel.startsWith("..") || path.isAbsolute(rel)) { throw new Error("Path escapes storage directory"); } return candidate; } ``` 3. Generate opaque server-side identifiers instead of accepting arbitrary path-related identifiers from clients. 4. Apply the validation consistently to approval, run-context, audit, Merkle, checkpoint, leaf, proof, and wrapper log paths. 5. Add regression tests containing `../`, `..\`, encoded separators, absolute paths, long identifiers, and platform-specific path syntax. 6. Run the service as a dedicated unprivileged account with access limited to its own data directory. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/vaibot-guard-service.mjs:19
Finding
Protected Guard Endpoints Fail Open When No Token Is Configured<![CDATA[ ## Vulnerability Details **File Location**: `scripts/vaibot-guard-service.mjs:19-20, 205-264` **Vulnerability Type**: Missing mandatory authentication **Risk Level**: High ### Vulnerable Code ```js const PORT = Number(process.env.VAIBOT_GUARD_PORT || 39111); const HOST = process.env.VAIBOT_GUARD_HOST || "127.0.0.1"; // Local service auth (recommended): when set, require bearer token for all mutating endpoints. const VAIBOT_GUARD_TOKEN = process.env.VAIBOT_GUARD_TOKEN || ""; function getBearer(req) { const h = req.headers["authorization"] || req.headers["Authorization"]; if (!h) return ""; const s = Array.isArray(h) ? h[0] : String(h); const m = s.match(/^Bearer\s+(.+)$/i); return m ? m[1].trim() : ""; } function requireAuth(req, res) { if (!VAIBOT_GUARD_TOKEN) return true; // auth disabled const bearer = getBearer(req); const alt = req.headers["x-vaibot-guard-token"]; const token = bearer || (Array.isArray(alt) ? alt[0] : (alt ? String(alt) : "")); if (token !== VAIBOT_GUARD_TOKEN) { json(res, 401, { ok: false, error: "Unauthorized" }); return false; } return true; } ``` ### Technical Analysis Authentication is optional and defaults to disabled. When `VAIBOT_GUARD_TOKEN` is empty, `requireAuth()` authorizes every request. The service exposes security-sensitive operations, including: - Tool and command decisions. - Approval enumeration and resolution. - Tool and execution finalization. - Checkpoint flushing and remote anchoring. - Inclusion-proof access. - Persistent audit and state-file writes. Although the default bind address is loopback, `VAIBOT_GUARD_HOST` is configurable. A deployment that binds to a non-loopback interface without setting a token exposes these operations to remote clients. Even on loopback, any local process can manipulate approvals and audit state. ### Attack Path 1. The operator starts the service without `VAIBOT_GUARD_TOKEN`. 2. The service binds to loopback or a configured network in ...[truncated 750 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Refuse startup without a sufficiently strong guard token: ```js if (!VAIBOT_GUARD_TOKEN || VAIBOT_GUARD_TOKEN.length < 32) { throw new Error("VAIBOT_GUARD_TOKEN must be configured"); } ``` 2. If unauthenticated development mode is necessary, require an explicit flag and enforce loopback binding. 3. Reject non-loopback binding unless authentication is enabled and transport protection is configured. 4. Use `crypto.timingSafeEqual()` for token comparison after validating equal lengths. 5. Consider a Unix-domain socket with restrictive filesystem permissions for local integration. 6. Separate approval-management credentials from ordinary decision/finalization credentials. 7. Add tests confirming that every non-health endpoint rejects unauthenticated requests. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/vaibot-guard-service.mjs:517
Finding
High-Risk Execution Tools Can Fall Through to an Allow Decision<![CDATA[ ## Vulnerability Details **File Location**: `scripts/vaibot-guard-service.mjs:517-619` **Vulnerability Type**: Policy enforcement bypass **Risk Level**: High ### Vulnerable Code ```js function classifyToolRisk({ toolName, params, workspaceDir }) { const tn = String(toolName || "").toLowerCase(); const url = extractUrlFromToolParams(params); // Network tools if (url && /^(https?:)?\/\//i.test(url)) { const allowlisted = isDomainAllowlisted(url); return allowlisted ? { risk: "high", reason: "network destination present (allowlisted)" } : { risk: "high", reason: "network destination not allowlisted" }; } if (tn.includes("web_fetch") || tn.includes("browser") || tn.includes("fetch")) { return { risk: "high", reason: "network/browsing tool" }; } // Explicit outbound messaging if (tn.startsWith("message") || tn.includes("message")) { return { risk: "high", reason: "outbound messaging tool" }; } // Shell / remote execution if (tn === "exec" || tn.includes("exec") || tn.includes("run")) { return { risk: "high", reason: "execution tool" }; } // File mutations (heuristic by tool name) if (/(^|\b)(write|edit|patch|apply|delete|rm|mkdir|upload)(\b|$)/i.test(tn)) { return { risk: "high", reason: "file mutation tool" }; } return { risk: "low", reason: "default low risk" }; } function decideTool({ sessionId, toolName, params, workspaceDir }) { const tn = String(toolName || ""); const joined = tn + " " + (() => { try { return JSON.stringify(params || {}); } catch { return "{unserializable:true}"; } })(); const deny = matchToken(DENY_TOKENS, joined); if (deny) return { decision: "deny", reason: `Denied token: ${deny}` }; const approve = matchToken(APPROVE_TOKENS, joined); if (approve) return { decision: "approve", reason: `Approval required for token: ${approve}`, approvalId: `appr_${randomUUID()}` }; const lower = tn.toLowerCase(); ...[truncated 2928 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Make authorization consume the computed risk classification. 2. Require approval or denial for every high-risk category; never allow a high-risk request through a default branch. 3. Add explicit branches for execution, remote-run, network, mutation, and outbound-messaging tools. 4. Change the final default to deny unknown tools or require approval: ```js if (risk.risk === "high") { return { decision: "approve", reason: risk.reason, approvalId: `appr_${randomUUID()}` }; } return { decision: "allow", reason: "Explicitly classified low risk" }; ``` 5. Normalize tool names against a centrally maintained capability registry instead of substring-only matching. 6. Reject mutation calls with no recognized path parameters rather than assuming they are safe. 7. Add tests for `exec`, `run`, `remote_run`, `patch`, `apply`, `rm`, `mkdir`, missing paths, unusual casing, and aliases. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/vaibot-guard-service.mjs:992
Finding
Default Redaction Rules Are Invalid and Sensitive Fields Are Not Recursively Sanitized<![CDATA[ ## Vulnerability Details **File Location**: `references/policy.default.json:23-30`; `scripts/vaibot-guard-service.mjs:992-1028` **Vulnerability Type**: Sensitive-data exposure through broken redaction **Risk Level**: High ### Vulnerable Code ```json "redactPatterns": [ "(?i)bearer\\s+[A-Za-z0-9._-]+", "(?i)api[_-]?key\\s*[:=]\\s*[^\\s]+", "(?i)token\\s*[:=]\\s*[^\\s]+", "(?i)secret\\s*[:=]\\s*[^\\s]+", "(?i)password\\s*[:=]\\s*[^\\s]+" ], "redactEnvKeyPatterns": ["(?i)key", "(?i)token", "(?i)secret", "(?i)pass"] ``` ```js function redactString(s) { let out = String(s); for (const pat of POLICY.redactPatterns) { try { out = out.replace(new RegExp(pat, "g"), "[REDACTED]"); } catch { // ignore bad patterns } } return out; } function redactIntent(intent) { if (!intent || typeof intent !== "object") return intent; const clone = JSON.parse(JSON.stringify(intent)); if (Array.isArray(clone.env_keys)) { clone.env_keys = clone.env_keys.map((k) => { const ks = String(k); const shouldRedact = POLICY.redactEnvKeyPatterns.some((p) => { try { return new RegExp(p).test(ks); } catch { return false; } }); return shouldRedact ? "[REDACTED_ENV_KEY]" : ks; }); } if (typeof clone.command === "string") clone.command = redactString(clone.command); if (Array.isArray(clone.args)) clone.args = clone.args.map((a) => redactString(a)); if (clone.network && Array.isArray(clone.network.destinations)) { clone.network.destinations = clone.network.destinations.map((d) => redactString(d)); } return clone; } ``` ### Technical Analysis JavaScript regular expressions do not support the inline `(?i)` case-insensitive modifier used by the default policy. Calls such as `new RegExp("(?i)token...", "g")` throw a syntax error. The implementation catches and silently ignores these errors. Consequently, the default secret-value patterns and environment-key patterns do not redact anythi ...[truncated 1299 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove inline modifiers from policy patterns and supply JavaScript flags explicitly: ```js new RegExp(pat, "gi") ``` 2. Compile and validate all redaction expressions during startup. Refuse to start when a configured expression is invalid. 3. Implement recursive redaction for arrays and objects. 4. Redact by property name as well as value pattern, including keys such as: `authorization`, `cookie`, `token`, `secret`, `password`, `apiKey`, and `privateKey`. 5. Avoid persisting raw command arguments and complete results unless explicitly necessary. 6. Apply redaction to the final serialized outbound body, not only selected source fields. 7. Add automated tests that place secrets in every supported nested structure and assert that neither logs nor mocked outbound requests contain them. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/vaibot-guard-service.mjs:685
Finding
Configurable Receipt Destination Permits Plaintext Transmission of API Credentials and Operational Telemetry<![CDATA[ ## Vulnerability Details **File Location**: `scripts/vaibot-guard-service.mjs:685-765`; `scripts/vaibot-guard.mjs:328-347` **Vulnerability Type**: Plaintext sensitive-data transmission and insufficient destination validation **Risk Level**: High ### Vulnerable Code ```js function postGovernanceReceipt({ runId, sessionId, intent, decision, risk, result, policyVersion }) { if (!VAIBOT_API_URL || !VAIBOT_API_KEY) return Promise.resolve(null); const toolName = String(intent?.toolName || intent?.tool || intent?.cmd?.split(" ")[0] || "unknown"); const command = String(intent?.cmd || intent?.command || toolName).slice(0, 500); const cwd = String(intent?.workspaceDir || intent?.cwd || "/"); const agentId = String(sessionId || "unknown-session"); const receiptPayload = { run_id: runId, idempotency_key: `${runId}:finalize`, agent: { id: agentId, name: agentId }, action: { tool: toolName, summary: `Agent ${actionVerb}: ${command.slice(0, 100)}`, command, cwd, }, policy: { risk_level: riskLevel, decision: mappedDecision, reason: String(decision?.reason || risk?.reason || "Policy decision"), }, approval: { status: approvalStatus }, result: { outcome, summary: String(decision?.reason || outcome), }, }; const baseUrl = VAIBOT_API_URL.replace(/\/api\/?$/, ""); const targetUrl = new URL(baseUrl + "/api/v2/receipts"); const body = JSON.stringify(receiptPayload); const options = { method: "POST", hostname: targetUrl.hostname, port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80), path: targetUrl.pathname, headers: { "content-type": "application/json; charset=utf-8", "content-length": Buffer.byteLength(body), "authorization": `Bearer ${VAIBOT_API_KEY}`, }, timeout: 8000, }; const transport = targetUrl.protocol === "https:" ? https : http; // request is sent using the selected transpor ...[truncated 2401 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Reject every destination whose protocol is not `https:`. 2. Maintain an explicit allowlist of approved API hostnames and ports. 3. Reject URLs containing user information, fragments, unexpected paths, or nonstandard ports unless explicitly permitted. 4. Consider certificate or public-key pinning for deployments with strict trust requirements. 5. Send checkpoint hashes or minimized summaries instead of raw command text, session identifiers, working directories, and result objects. 6. Make detailed telemetry a separate, explicit opt-in from cryptographic anchoring. 7. Protect the environment file from modification and verify ownership and mode before loading it. 8. Use a distinct, narrowly scoped credential for receipt submission. ]]>

T06 · System Persistence

Warning
Location
scripts/vaibot-guard.mjs:239
Finding
Optional Installer Creates a Cross-Session systemd Service<![CDATA[ ## Vulnerability Details **File Location**: `scripts/vaibot-guard.mjs:239-304`; `references/ops-runbook.md:66-106` **Vulnerability Type**: Persistent service registration **Risk Level**: Medium ### Vulnerable Code ```js async function cmdInstallLocal() { if (!process.stdin.isTTY) { die("install-local requires a TTY"); } const unitTemplate = `[Unit]\n` + `Description=VAIBot Guard policy service (user)\n` + `After=network-online.target openclaw-gateway.service\n` + `Wants=openclaw-gateway.service\n` + `PartOf=openclaw-gateway.service\n\n` + `[Service]\n` + `Type=simple\n` + `WorkingDirectory=%h/clawd/skills/openclaw-guard-skill\n` + `EnvironmentFile=%h/.config/vaibot-guard/vaibot-guard.env\n` + `ExecStart=/usr/bin/env node scripts/vaibot-guard-service.mjs\n` + `Restart=on-failure\n` + `RestartSec=2\n\n` + `NoNewPrivileges=true\n` + `PrivateTmp=true\n\n` + `[Install]\n` + `WantedBy=default.target\n`; const unitDstDir = path.join(os.homedir(), ".config", "systemd", "user"); const unitDst = path.join(unitDstDir, "vaibot-guard.service"); fs.mkdirSync(unitDstDir, { recursive: true }); fs.writeFileSync(unitDst, unitTemplate); const ans3 = (await rl.question( "Enable + start vaibot-guard now (systemctl --user enable --now)? [y/N]: " )).trim().toLowerCase(); if (ans3 === "y" || ans3 === "yes") { const { execSync } = await import("node:child_process"); execSync("systemctl --user daemon-reload", { stdio: "inherit" }); execSync("systemctl --user enable --now vaibot-guard", { stdio: "inherit" }); execSync("systemctl --user status vaibot-guard --no-pager", { stdio: "inherit" }); } } ``` The runbook also documents system-level installation: ```bash sudo systemctl daemon-reload sudo systemctl enable --now vaibot-guard sudo systemctl status vaibot-guard ``` ### Technical Analysis The installer writes a systemd user unit and can enable it for automatic ...[truncated 1631 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Keep foreground execution as the default and present persistence as a separately reviewed deployment option. 2. Install service code into a versioned, non-user-writable or integrity-verified location rather than executing directly from a mutable Skill directory. 3. Pin `ExecStart` to an exact Node.js and script path. 4. Add stronger systemd sandboxing, including appropriate combinations of: - `ProtectSystem=strict` - `ProtectHome=read-only` - `ReadWritePaths=` limited to the guard data directory - `RestrictAddressFamilies=` - `PrivateDevices=true` - `CapabilityBoundingSet=` 5. Ensure system service examples specify a dedicated unprivileged `User=` and `Group=`. 6. Provide a documented uninstall operation that disables the unit and removes generated files. 7. Verify unit and environment-file ownership and permissions before startup. 8. Require an additional confirmation before presenting or executing system-wide installation instructions. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/vaibot-guard-exec.mjs:45
Finding
Standalone Execution Wrapper Omits Authentication and Sends an Incompatible Request Schema<![CDATA[ ## Vulnerability Details **File Location**: `scripts/vaibot-guard-exec.mjs:45-57, 81-111` **Vulnerability Type**: Security-wrapper integration failure **Risk Level**: Medium ### Vulnerable Code ```js function requestDecision(payload) { const body = JSON.stringify(payload); const options = { hostname: GUARD_HOST, port: GUARD_PORT, path: "/v1/decide/exec", method: "POST", headers: { "content-type": "application/json; charset=utf-8", "content-length": Buffer.byteLength(body), }, timeout: 5000, }; // ... } // Run plan is required. Provide either: // - env VAIBOT_RUN_PLAN (JSON) // - argv[2] as JSON const runPlanJson = process.env.VAIBOT_RUN_PLAN || process.argv[2]; // ... const decisionResp = await requestDecision({ sessionId, cmd, args, runPlan, }); ``` The service expects a field named `intent` and validates it: ```js function validateIntent(intent) { if (!intent || typeof intent !== "object") return "Missing intent"; const required = ["tool", "action", "command", "cwd"]; for (const k of required) { if (!(k in intent)) return `intent missing field: ${k}`; } return null; } ``` ### Technical Analysis The documented standalone execution wrapper sends `runPlan`, while the decision service reads `input.intent`. It also omits the `Authorization` header required when the recommended guard token is configured. As a result, the wrapper cannot successfully use the service under the recommended authenticated configuration and cannot satisfy intent validation even when authentication is disabled. This is primarily an availability and enforcement-integrity defect rather than direct code execution. Because `references/policy.md` states that all execution must go through this wrapper, wrapper failure may encourage operators or integrations to bypass the guard and invoke commands directly. ### Attack Path 1. The operator configures the recommended `VAIBOT_GUARD_TOKEN`. 2. A command is in ...[truncated 788 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Send the documented field name and schema: ```js const decisionResp = await requestDecision({ sessionId, cmd, args, intent: runPlan, }); ``` 2. Load the guard token using the same environment-file logic as the main CLI. 3. Add `Authorization: Bearer <token>` when a token is configured. 4. Consolidate request construction into a shared client module so the CLI and wrapper cannot diverge. 5. Add an integration test that starts an authenticated service and executes the complete wrapper decision flow. 6. Update documentation so the run-plan schema and service intent schema are identical. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (32)

Ae1

High
Category
analysis-evasion
Content
node scripts/vaibot-guard-service.mjs
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/vaibot-guard.mjs install-local
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Self-Modification

High
Category
Rogue Agent
Content
## Shared environment variables

Retention:
- `VAIBOT_LOG_RETENTION_DAYS` (default 14): remove guard log/state files in `VAIBOT_GUARD_LOG_DIR` older than this many days.

Common env vars (see `SKILL.md` for full list):
- `VAIBOT_GUARD_HOST` (default `127.0.0.1`)
Confidence
70% 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.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The file downplays the skill as only a distribution and UX surface, but the skill metadata describes operational capabilities including running the guard service, managing approvals, and validating receipts or audit logs. This mismatch can mislead reviewers, operators, or marketplace consumers about the effective trust boundary and authority of the skill, causing under-review of privileged behavior and unsafe deployment decisions.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill describes capabilities that require environment-variable access and shell execution, but it does not declare any explicit tool scope such as permissions or allowed-tools. That creates an authorization gap: an agent or runtime may expose more capability than reviewers expect, making it easier for the skill to start services, modify user configuration, or invoke local commands without clear boundary declarations.

Session Persistence

Medium
Category
Rogue Agent
Content
# OpenClaw Guard Skill (VAIBot v2.1)

Provide a **local policy decision service** plus a CLI to gate OpenClaw tool calls and write **tamper-evident audit logs** in `.vaibot-guard/`.

## Sensitive credentials
- `VAIBOT_GUARD_TOKEN` — bearer token for Guard endpoints (recommended)
Confidence
84% confidence
Finding
The skill explicitly instructs writing tamper-evident audit logs under a persistent local directory and also references sensitive credentials used by the guard service. Persistent logging is not inherently malicious, but in this context it can retain sensitive operational data, approval decisions, command metadata, or tokens if implementations are careless, increasing the blast radius of local compromise or accidental disclosure.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
Two options are provided:
- **Local workstation mode (recommended):** user service (`systemctl --user`)
- **VPS / production mode:** system service (`sudo systemctl`) — **templates are not shipped in the ClawHub skill artifact**; generate/copy your own unit file (see notes below).

## Shared environment variables
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Session Persistence

Medium
Category
Rogue Agent
Content
```bash
systemctl --user daemon-reload
systemctl --user enable --now vaibot-guard
systemctl --user status vaibot-guard
```
Confidence
82% confidence
Finding
`systemctl --user enable --now vaibot-guard` creates user-level persistence by both starting the service and enabling it for future sessions. In a skill context, persistence is more sensitive because users may execute setup steps without fully understanding they are installing a background service tied to their account.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
Notes:
- The installer writes:
  - `~/.config/systemd/user/vaibot-guard.service`
  - `~/.config/vaibot-guard/vaibot-guard.env` (recommended `chmod 600`)
- Systemd templates are provided under `references/systemd/` for reference; do not assume `.service` files ship in the ClawHub artifact.

### Logs
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
Notes:
- The installer writes:
  - `~/.config/systemd/user/vaibot-guard.service`
  - `~/.config/vaibot-guard/vaibot-guard.env` (recommended `chmod 600`)
- Systemd templates are provided under `references/systemd/` for reference; do not assume `.service` files ship in the ClawHub artifact.

### Logs
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
Notes:
- The installer writes:
  - `~/.config/systemd/user/vaibot-guard.service`
  - `~/.config/vaibot-guard/vaibot-guard.env` (recommended `chmod 600`)
- Systemd templates are provided under `references/systemd/` for reference; do not assume `.service` files ship in the ClawHub artifact.

### Logs
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
1) Create a dedicated user:

```bash
sudo useradd --system --create-home --home-dir /var/lib/vaibot-guard --shell /usr/sbin/nologin vaibot-guard
```

2) Create `/etc/vaibot-guard/vaibot-guard.env` and set required env vars (at minimum `VAIBOT_GUARD_TOKEN`, `VAIBOT_POLICY_PATH`, `VAIBOT_GUARD_LOG_DIR`).
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
1) Create a dedicated user:

```bash
sudo useradd --system --create-home --home-dir /var/lib/vaibot-guard --shell /usr/sbin/nologin vaibot-guard
```

2) Create `/etc/vaibot-guard/vaibot-guard.env` and set required env vars (at minimum `VAIBOT_GUARD_TOKEN`, `VAIBOT_POLICY_PATH`, `VAIBOT_GUARD_LOG_DIR`).
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
1) Create a dedicated user:

```bash
sudo useradd --system --create-home --home-dir /var/lib/vaibot-guard --shell /usr/sbin/nologin vaibot-guard
```

2) Create `/etc/vaibot-guard/vaibot-guard.env` and set required env vars (at minimum `VAIBOT_GUARD_TOKEN`, `VAIBOT_POLICY_PATH`, `VAIBOT_GUARD_LOG_DIR`).
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
1) Create a dedicated user:

```bash
sudo useradd --system --create-home --home-dir /var/lib/vaibot-guard --shell /usr/sbin/nologin vaibot-guard
```

2) Create `/etc/vaibot-guard/vaibot-guard.env` and set required env vars (at minimum `VAIBOT_GUARD_TOKEN`, `VAIBOT_POLICY_PATH`, `VAIBOT_GUARD_LOG_DIR`).
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now vaibot-guard
sudo systemctl status vaibot-guard
```
Confidence
75% confidence
Finding
`sudo systemctl enable --now vaibot-guard` establishes persistence by starting the service immediately and enabling it on boot. While legitimate for service installation, this creates durable execution, which is security-relevant in a skill artifact because it can outlive the invoking session if the service or its configuration is later compromised.

Session Persistence

Medium
Category
Rogue Agent
Content
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now vaibot-guard
sudo systemctl status vaibot-guard
```
Confidence
90% confidence
Finding
This command enables system-wide boot persistence for the guard service, causing it to run automatically across reboots under systemd. In the context of a skill artifact, system-level persistence is materially more dangerous than ordinary documentation because it can institutionalize long-lived execution on a host if the service, policy, or environment file is misconfigured or later abused.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The service is described as a local guard, but it is explicitly configured to send proof receipts and audit-derived data to remote VAIBot endpoints when VAIBOT_API_URL and VAIBOT_API_KEY are set. Even with some redaction, this can transmit execution intent, commands, paths, results, and session metadata off-host, creating a confidentiality and trust-boundary issue if operators expect strictly local processing.

Unrestricted Tool Access

Medium
Category
Excessive Agency
Content
function isDomainAllowlisted(dest) {
  // Tight allowlist semantics:
  // - If allowlist is empty: allow all (legacy behavior).
  // - Exact host match is allowed.
  // - Subdomains are ONLY allowed when the allowlist entry is explicitly wildcarded as "*.example.com".
  if (ALLOWLISTED_DOMAINS.length === 0) return true;
Confidence
97% confidence
Finding
If ALLOWLISTED_DOMAINS is empty, isDomainAllowlisted returns true for any destination, effectively disabling outbound network restrictions. In a guard service that is supposed to enforce tool decisions and constrain risky actions, this fail-open behavior can allow arbitrary egress, remote fetches, and data exfiltration without the intended approval barrier.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The proof submission path sends receipt contents, including redacted intent and audit data, to a remote /prove service using a bearer token. Because this occurs automatically and the service is framed as local policy enforcement, it creates a meaningful risk of unintended disclosure of sensitive operational context.

Context-Inappropriate Capability

Medium
Confidence
88% confidence
Finding
The governance receipt uploader sends structured execution metadata to an external API using a bearer credential, which expands the system from a local guard into a telemetry/reporting component. If enabled without strong operator awareness, it can leak sensitive command summaries, cwd values, policy decisions, and session identifiers to a third party.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
This code performs credential-backed network transmission to a remote receipts API using VAIBOT_API_KEY, but there is no user-facing disclosure at the point of use that execution metadata will be uploaded. Silent background exfiltration of operational metadata is risky in a guard service because users may assume decisions and logs remain local.

Session Persistence

Medium
Category
Rogue Agent
Content
if (tok.created) {
    console.error(`Generated VAIBOT_GUARD_TOKEN and wrote ${envFile} (chmod 600).`);

    // If systemd user unit appears installed, offer to restart it so the running service picks up the new token.
    const userUnitPath = path.join(os.homedir(), ".config", "systemd", "user", "vaibot-guard.service");
    const hasUnit = fs.existsSync(userUnitPath);
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.

Session Persistence

Medium
Category
Rogue Agent
Content
if (tok.created) {
    console.error(`Generated VAIBOT_GUARD_TOKEN and wrote ${envFile} (chmod 600).`);

    // If systemd user unit appears installed, offer to restart it so the running service picks up the new token.
    const userUnitPath = path.join(os.homedir(), ".config", "systemd", "user", "vaibot-guard.service");
    const hasUnit = fs.existsSync(userUnitPath);
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.

Session Persistence

Medium
Category
Rogue Agent
Content
// cmdConfigure exits on success; if it doesn't, continue.
    }

    const ans3 = (await rl.question("Enable + start vaibot-guard now (systemctl --user enable --now)? [y/N]: ")).trim().toLowerCase();
    if (ans3 === "y" || ans3 === "yes") {
      const { execSync } = await import("node:child_process");
      execSync("systemctl --user daemon-reload", { stdio: "inherit" });
Confidence
94% confidence
Finding
This code offers to enable and start a systemd user service, which creates persistence across user sessions. Although it is gated by an interactive prompt and consistent with the skill's stated purpose, persistence mechanisms are security-sensitive because a compromised or misconfigured guard service would continue running automatically and could mediate future tool decisions.

Static analysis

Detected: suspicious.dangerous_exec, suspicious.env_credential_access, suspicious.exposed_secret_literal

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/vaibot-guard-exec.mjs:3

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/vaibot-guard.mjs:201

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
tests/guard-service.test.mjs:32

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
scripts/vaibot-guard-exec.mjs:18

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
scripts/vaibot-guard.mjs:20

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
tests/guard-service.test.mjs:22

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
scripts/vaibot-guard.mjs:330