Back to skill

Security audit

Arbiter

Security checks for vulnerabilities and agentic risk

Overview

This skill has a coherent human-review workflow, but its local queue handling can write or expose decision files more broadly than users are told.

Review before installing. Do not send secrets, credentials, personal data, regulated data, or sensitive internal plans through this skill. Use it only on a trusted single-user machine or after fixing path validation, YAML/Markdown serialization, exact plan matching, ownership checks, and restrictive file permissions.

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

T09 · Insecure Skill Coding Practices

Error
Location
src/push.ts:110
Finding
Path Traversal Through Attacker-Controlled Agent Identifier<![CDATA[ ## Vulnerability Details **File Location**: `src/push.ts`, lines 110-125 **Vulnerability Type**: Path traversal and arbitrary file placement **Risk Level**: High ### Vulnerable Code ```typescript const id = nanoid(8); const agent = args.agent || process.env.CLAWDBOT_AGENT || 'unknown'; const session = args.session || process.env.CLAWDBOT_SESSION || 'unknown'; const filename = `${agent}-${slugify(args.title)}-${id}.md`; const filepath = join(getQueueDir('pending'), filename); const content = generateMarkdown({ ...args, id, agent, session }); ensureQueueDirs(); writeFileSync(filepath, content, 'utf-8'); ``` ### Technical Analysis The attacker-controlled `args.agent` value is inserted directly at the beginning of the filename. Unlike the title, the agent value is not passed through `slugify()` or otherwise restricted to a safe filename component. Node.js path normalization processes `../` segments contained in `agent`. Consequently, `join(getQueueDir('pending'), filename)` may resolve outside the intended `~/.arbiter/queue/pending` directory. The random plan identifier prevents an attacker from selecting the complete final filename, but it does not prevent directory traversal or arbitrary placement of a new Markdown file in another writable directory. The file content is also largely attacker-controlled. ### Attack Path 1. An attacker who can invoke `arbiter-push` supplies an agent identifier containing traversal components, for example: ```bash arbiter-push '{ "agent":"../../../../tmp/attacker-output", "title":"payload", "decisions":[{ "id":"d1", "title":"Test", "context":"Content", "options":[{"key":"a","label":"A"}] }] }' ``` 2. The application builds a filename beginning with: ```text ../../../../tmp/attacker-output-payload-<random-id>.md ``` 3. `join()` normalizes the path and resolves it outside the pending queue. 4. `writeFileSync()` creates the attacker-con ...[truncated 921 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Treat `agent` as a logical identifier, not as a path component. - Apply a strict allowlist, such as `^[A-Za-z0-9_-]{1,64}$`. - Slugify or reject unsafe values rather than silently accepting separators. - Resolve the destination and verify that it remains inside the expected directory: ```typescript import { resolve, sep } from 'node:path'; const queueDir = resolve(getQueueDir('pending')); const safeAgent = validateAgent(args.agent); const filepath = resolve(queueDir, `${safeAgent}-${slugify(args.title)}-${id}.md`); if (!filepath.startsWith(queueDir + sep)) { throw new Error('Invalid output path'); } ``` - Open the file with exclusive creation (`wx`) to avoid accidental replacement. - Add tests using `../`, absolute paths, backslashes, Unicode separators, and empty identifiers. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
src/push.ts:39
Finding
YAML Frontmatter and Markdown Structure Injection<![CDATA[ ## Vulnerability Details **File Location**: `src/push.ts`, lines 39-85 **Vulnerability Type**: Stored structured-data injection **Risk Level**: High ### Vulnerable Code ```typescript const frontmatter = `--- id: ${args.id} version: 1 agent: ${args.agent} session: ${args.session} tag: ${args.tag || 'general'} title: "${args.title}" priority: ${args.priority || 'normal'} status: pending created_at: ${now} updated_at: ${now} completed_at: null total: ${total} answered: 0 remaining: ${total} ${args.notify ? `notify_session: ${args.notify}` : ''} ---`; const contextSection = ` # ${args.title} ${args.context || 'Please review and answer the following decisions.'} `; const decisionSections = args.decisions.map((d, i) => { const optionsMarkdown = d.options .map(o => `- \`${o.key}\` — ${o.label}${o.note ? ` (${o.note})` : ''}`) .join('\n'); return ` --- ## Decision ${i + 1}: ${d.title} id: ${d.id} status: pending answer: null answered_at: null ${d.allowCustom ? 'allow_custom: true' : ''} **Context:** ${d.context} **Options:** ${optionsMarkdown} `; }).join('\n'); ``` ### Technical Analysis Untrusted fields are interpolated directly into YAML frontmatter and into the Markdown control structure without escaping or schema validation. Affected values include: - `agent` - `session` - `tag` - `title` - `priority` - `notify` - Decision IDs and titles - Decision context - Option keys, labels, and notes Newline characters and YAML delimiters can introduce new metadata fields, terminate frontmatter early, or alter how downstream components parse the document. Markdown separators and lines such as `id:`, `status:`, and `answer:` can also create forged decision blocks because `parseDecisions()` later relies on regular expressions over text sections rather than a strict structured format. TypeScript interfaces provide no runtime protection because input is accepted through `JSON.parse()` and cast directly to `PushArgs`. ### Attack Path 1. An atta ...[truncated 1426 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Never construct YAML through string interpolation. - Build a plain metadata object and serialize it with a maintained YAML serializer or `gray-matter.stringify()`. - Apply runtime schema validation with strict field types, lengths, enumerations, and character restrictions. - Restrict identifiers and routing fields to safe allowlists. - Validate `priority` against `low`, `normal`, `high`, and `urgent`. - Reject carriage returns, newlines, YAML delimiters, and control characters in scalar identifiers. - Escape Markdown metacharacters in display text or use a renderer that does not interpret embedded HTML. - Replace the regex-based decision storage format with a strictly parsed representation, such as JSON or YAML arrays. - Reject unknown input properties to reduce metadata smuggling. - Add adversarial tests for `---`, quotes, newlines, duplicate YAML keys, `id:`, `answer:`, and `status:` payloads. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
src/utils.ts:52
Finding
Partial Plan-ID Matching Can Return the Wrong Plan and Disclose Answers<![CDATA[ ## Vulnerability Details **File Location**: `src/utils.ts`, lines 52-82 **Vulnerability Type**: Improper authorization and non-exact identifier matching **Risk Level**: Medium ### Vulnerable Code ```typescript export function findPlanFile(planId?: string, tag?: string): string | null { const dirs = [getQueueDir('pending'), getQueueDir('completed')]; for (const dir of dirs) { if (!existsSync(dir)) continue; const files = readdirSync(dir).filter(f => f.endsWith('.md')); for (const file of files) { const filepath = join(dir, file); // Quick check by filename for planId if (planId && file.includes(planId)) { return filepath; } // Need to read file for tag match if (tag) { try { const content = readFileSync(filepath, 'utf-8'); const { data } = matter(content); if (data.tag === tag || data.id === planId) { return filepath; } } catch { continue; } } } } return null; } ``` ### Technical Analysis Plan identifiers are matched with `file.includes(planId)` rather than by exact metadata equality. An attacker does not need to know the full eight-character plan ID; any substring appearing in a filename can select a plan. The first matching file returned by `readdirSync()` wins. This produces ambiguous, order-dependent lookup behavior. The issue affects both `arbiter-status` and `arbiter-get`, and the latter returns completed decision answers. No ownership check compares the selected plan's `agent` or `session` metadata with the requesting agent or session. In a shared queue used by multiple agents under the same operating-system account, one agent can query another agent's plan. ### Attack Path 1. Multiple agents create plans in the shared `~/.arbiter/queue` hierarchy. 2. A caller invokes `arbiter-get` or `arbiter-status` using a short identifier, filename fragment, agent name, or title fragment. ...[truncated 843 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove substring matching entirely. - Parse candidate frontmatter and require exact equality: ```typescript if (planId && data.id === planId) { return filepath; } ``` - Validate plan IDs against the exact Nano ID format and expected length before searching. - If filename lookup is retained for performance, require an exact suffix pattern such as `-${planId}.md`. - Detect duplicate tags and require an unambiguous selection rather than returning the first match. - Enforce ownership by comparing the document's `agent` or `session` with an authenticated caller identity. - Separate queues by agent or session when different agents should not access one another's plans. - Return an ambiguity error if multiple records match. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
src/utils.ts:28
Finding
Queue Directories and Decision Files Are Created Without Explicit Restrictive Permissions<![CDATA[ ## Vulnerability Details **File Location**: `src/utils.ts`, lines 28-35; `src/push.ts`, lines 124-125 **Vulnerability Type**: Insecure filesystem permissions **Risk Level**: Medium ### Vulnerable Code ```typescript export function ensureQueueDirs(): void { const dirs = ['pending', 'completed', 'notify'] as const; for (const dir of dirs) { const path = getQueueDir(dir); if (!existsSync(path)) { mkdirSync(path, { recursive: true }); } } } ``` ```typescript ensureQueueDirs(); writeFileSync(filepath, content, 'utf-8'); ``` ### Technical Analysis Neither directory creation nor file creation specifies a restrictive mode. Effective permissions therefore depend entirely on the process umask. With a common umask of `022`, directories are generally created as `0755` and files as `0644`. On a multi-user system, other local users may be able to list queue directories and read plans containing human decisions, session identifiers, notification destinations, and project context. A permissive or misconfigured umask can make the exposure broader. The application does not verify permissions on an existing `~/.arbiter` hierarchy either. ### Attack Path 1. The user runs `arbiter-push` on a multi-user host. 2. `ensureQueueDirs()` creates the queue hierarchy using default permissions. 3. `writeFileSync()` creates a plan using default file permissions. 4. Another local user traverses the user's home directory and reads the queue file if the resulting modes and parent-directory permissions allow it. 5. The local user obtains plan context, routing metadata, and potentially completed human answers. ### Impact Assessment The exposure is limited to users who can access the relevant filesystem hierarchy, but the affected data may include sensitive architectural choices, approval decisions, session names, and free-text answers. The process does not gain additional privileges; the principal impact is local confidentiality loss and possible queue ...[truncated 68 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Create the base and queue directories with mode `0700`. - Create plan files with mode `0600`. - Use exclusive file creation: ```typescript mkdirSync(path, { recursive: true, mode: 0o700 }); writeFileSync(filepath, content, { encoding: 'utf-8', mode: 0o600, flag: 'wx' }); ``` - Verify and correct permissions on existing directories and files. - Reject queue paths owned by a different operating-system user. - Document that plans can contain sensitive information and should not be placed in shared directories. - Consider setting a restrictive process umask during queue operations, while still specifying explicit modes. ]]>

T08 · Insecure Dependencies

Note
Location
package.json:17
Finding
Dependency Resolution Is Not Reproducibly Locked<![CDATA[ ## Vulnerability Details **File Location**: `package.json`, lines 17-23 **Vulnerability Type**: Supply-chain dependency drift **Risk Level**: Low ### Vulnerable Code ```json "dependencies": { "gray-matter": "^4.0.3", "nanoid": "^5.0.4" }, "devDependencies": { "typescript": "^5.3.3", "@types/node": "^20.11.0" } ``` No package lockfile is present in the audited project structure. ### Technical Analysis Caret version ranges permit future compatible releases to be selected during installation. Without a committed lockfile, two installations of the same project can resolve different dependency versions. This does not establish that any listed package is malicious. However, it weakens reproducibility and increases exposure to a future compromised, malicious, or unexpectedly vulnerable release that still satisfies the declared range. ### Attack Path 1. A dependency maintainer account or package registry entry is compromised, or a vulnerable compatible release is published. 2. The new release satisfies one of the caret ranges in `package.json`. 3. A user installs or builds the skill without a lockfile. 4. The package manager resolves and installs the new release. 5. Dependency installation or runtime imports execute the changed third-party code with the installing user's privileges. ### Impact Assessment If an accepted dependency release is compromised, the dependency runs with the same privileges as the CLI or build process. This can potentially expose queue contents, environment variables, and user-accessible files. The current repository provides no evidence that the named packages are malicious, so this finding concerns supply-chain hardening and reproducibility rather than a confirmed malicious dependency. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions - Generate and commit the package manager's lockfile. - Use deterministic CI installation, such as `npm ci` or the corresponding frozen-lockfile option. - Review dependency updates before regenerating the lockfile. - Enable automated vulnerability and provenance scanning. - Pin release automation to reviewed dependency artifacts. - Consider exact versions for security-sensitive dependencies while maintaining a controlled update process. - Verify package signatures or registry provenance where supported. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (15)

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The description says this skill submits decisions to Arbiter Zebu for async human review. However, the provided code chunk is for a separate 'get' operation and does not submit anything. In fact, it does not perform retrieval either; it is only a placeholder that prints a not-implemented message and exits. This is a material mismatch in primary purpose and capability.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The description claims the skill pushes decisions to Arbiter Zebu for async human review. However, the actual code does not perform any submission, file creation, queue interaction, or review-routing behavior. It only prints a 'not yet implemented' message and exits with an error. This is a material mismatch in primary behavior, even though the comments describe an intended future implementation.

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding
The description says the skill submits or pushes decisions to Arbiter Zebu for async human review. The actual code chunk does not do that. It is a stub for a different function: checking the status of a plan/decision file, and even that functionality is not implemented. Its primary purpose and behavior therefore differ materially from the declared description.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The declared description says this skill is for pushing decisions to Arbiter Zebu for async human review. However, the provided code implements a `get` command that looks up an existing plan file, checks whether its status is `completed`, parses decisions, and returns collected answers as JSON. Its primary purpose is result retrieval, not submission for human review. This is a material purpose mismatch, even though both relate to the same broader Arbiter workflow.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
This is a material description-behavior mismatch. The declared purpose suggests an action-oriented submission skill that sends decisions to Arbiter Zebu for async human review. The supplied code does something different: it is a read-only status inspection utility for previously created plans. It locates a plan file, parses stored metadata and decisions, and prints a JSON status report. There is no evidence of pushing requests, initiating human review, or requesting approval. The code’s primary purpose, resources accessed (local plan files), and invocation triggers differ from the declaration.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The README shows users passing potentially sensitive decision context into `arbiter-push` and explains that a human reviews answers in Telegram, but it does not clearly warn that submitted content leaves the agent boundary and is exposed to a human-operated external review channel. In an agent skill context, this omission can cause operators or upstream agents to send secrets, internal architecture details, credentials, or regulated data for review without informed consent, resulting in confidentiality and privacy leakage.

Lp3

Medium
Category
MCP Least Privilege
Confidence
86% confidence
Finding
The skill explicitly relies on environment variables (`CLAWDBOT_AGENT`, `CLAWDBOT_SESSION`) and local CLI execution, but it does not declare tool scope or permissions. That can cause the agent runtime to grant broader access than users expect and obscures that the skill reads execution context from the environment.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The skill sends plan context, decision details, and notifications into local queue files for later human review, but the description does not clearly warn users that potentially sensitive content will be persisted on disk and exposed to a human operator. In agent workflows, this can lead to inadvertent disclosure of secrets, private data, or internal plans.

Session Persistence

Medium
Category
Rogue Agent
Content
#!/usr/bin/env bash
#
# arbiter push - Create a decision file in the queue
#
# Usage: push.sh <tag> <title> [options]
#
Confidence
60% 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.

Missing User Warnings

Low
Confidence
84% confidence
Finding
The heartbeat guidance tells the agent to delete notification files after processing without warning that deletion is destructive and may remove evidence needed for audit, troubleshooting, or recovery. In multi-agent or asynchronous systems, premature deletion can also cause missed notifications or state inconsistencies.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"get": "node dist/get.js"
  },
  "dependencies": {
    "gray-matter": "^4.0.3",
    "nanoid": "^5.0.4"
  },
  "devDependencies": {
Confidence
92% confidence
Finding
The dependency gray-matter is specified with a caret range, which permits automatic installation of newer minor/patch releases than the one reviewed by the author. This weakens build reproducibility and increases supply-chain risk because a later compromised or breaking release could be pulled in without an explicit manifest change.

Unpinned Dependencies

Low
Category
Supply Chain
Content
},
  "dependencies": {
    "gray-matter": "^4.0.3",
    "nanoid": "^5.0.4"
  },
  "devDependencies": {
    "typescript": "^5.3.3",
Confidence
95% confidence
Finding
The nanoid dependency is unpinned via a caret range, so consumers may resolve different releases over time. In a security-relevant package, this creates avoidable supply-chain and reproducibility risk, especially because advisory exposure cannot be confidently ruled out when the exact installed version is unspecified.

Unverifiable Dependency: nanoid has 5 known advisory(ies) (CVE-2026-67214 (nanoid: non-secure generators can loop indefinitely with negative size); CVE-2026-67213 (nanoid: custom generators can loop indefinitely when size is zero); CVE-2024-55565 (Predictable results in nanoid generation when given non-integer values) +2 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
89% confidence
Finding
The manifest references nanoid without an exact pinned version, while the package has known advisories in some releases. Because the actual installed version cannot be verified from this manifest alone, the project may resolve to an affected release, creating uncertainty around ID-generation safety and potential denial-of-service or predictability issues depending on usage.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"nanoid": "^5.0.4"
  },
  "devDependencies": {
    "typescript": "^5.3.3",
    "@types/node": "^20.11.0"
  },
  "engines": {
Confidence
83% confidence
Finding
TypeScript is a development dependency, but leaving it unpinned still permits non-deterministic toolchain changes during builds. That can introduce build instability or, in a supply-chain compromise scenario, malicious code execution during development or CI installation.

Unpinned Dependencies

Low
Category
Supply Chain
Content
},
  "devDependencies": {
    "typescript": "^5.3.3",
    "@types/node": "^20.11.0"
  },
  "engines": {
    "node": ">=20.0.0"
Confidence
80% confidence
Finding
The @types/node package is unpinned, allowing build-time type definitions to drift across installations. While lower risk than runtime packages, it still reduces reproducibility and can contribute to supply-chain exposure in developer and CI environments.

Static analysis

No suspicious patterns detected.