Back to skill

Security audit

Payclaw

Security checks for vulnerabilities and agentic risk

Overview

PayClaw is a coherent payments skill, but it has high-impact safety issues in command execution, credential handling, and misleading escrow behavior that users should review before installing.

Review this carefully before installing. Treat the escrow feature as local bookkeeping, not as real locked or trustless escrow. Do not use it for paid work unless the implementation is changed to lock funds and verify releases/refunds. Avoid entering real payment credentials until command execution is hardened and secret storage is improved; use testnet-only credentials and manually verify every destination and amount.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
src/cli.ts:88
Finding
Shell Command Injection Through User-Controlled CLI Arguments<![CDATA[ ## Vulnerability Details **File Location**: `src/cli.ts:88-91, 109, 135, 188, 339` **Vulnerability Type**: OS command injection **Risk Level**: High ### Vulnerable Code ```typescript // Run circle-wallet command function circleWallet(cmd: string): string { try { return execSync(`circle-wallet ${cmd}`, { encoding: 'utf-8' }); } catch (e: any) { throw new Error(e.stderr || e.message); } } ``` User-controlled values reach this function at several call sites: ```typescript circleWallet(`setup --api-key ${options.apiKey}`); ``` ```typescript const result = circleWallet(`create "${name || 'PayClaw Wallet'}"`); ``` ```typescript const result = circleWallet(`send ${address} ${amount}`); ``` ```typescript const result = circleWallet(`send ${escrow.recipient} ${escrow.amount}`); ``` ### Technical Analysis `execSync()` executes the interpolated string through a system shell. Values originating from command-line arguments—including the API key, wallet name, payment address, payment amount, and stored escrow recipient—are inserted into that string without shell-safe argument separation. The quotation marks around the wallet name do not prevent exploitation. An attacker can include a closing quotation mark followed by shell metacharacters or command substitution syntax. Unquoted values such as `address`, `amount`, and `apiKey` are directly exposed to shell parsing. The escrow release path is also vulnerable because escrow records are loaded from mutable local JSON and their `recipient` and `amount` fields are passed to the same command execution sink. ### Attack Path 1. An attacker supplies a crafted value through a CLI parameter, such as a wallet name, API key, destination address, or amount. 2. Alternatively, an attacker modifies `~/.openclaw/payclaw/escrows.json` and places shell syntax in an escrow recipient. 3. PayClaw interpolates the malicious value into a `circle-wallet` command string. 4. `execSync()` passes the resulting string ...[truncated 930 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Replace shell-string execution with argument-array execution: ```typescript import { execFileSync } from 'child_process'; function circleWallet(args: string[]): string { try { return execFileSync('circle-wallet', args, { encoding: 'utf-8', shell: false }); } catch (e: any) { throw new Error(e.stderr || e.message); } } ``` - Pass every argument as a separate array element: ```typescript circleWallet(['send', address, amount]); circleWallet(['create', name || 'PayClaw Wallet']); ``` - Validate destination addresses against the exact address format supported by the selected chain. - Require amounts to be finite, positive numbers within an explicitly defined range and serialize them canonically. - Allowlist supported chain identifiers. - Validate escrow records again after loading them from disk; never trust persisted JSON merely because the application created it. - Do not place secrets in command-line arguments. Pass the API key through protected standard input or a documented secure environment mechanism supported by `circle-wallet`. - Add automated tests containing shell metacharacters, substitutions, quotes, and newline characters to verify that inputs cannot alter the invoked executable or argument boundaries. ]]>

other

Error
Location
src/cli.ts:274
Finding
Advertised Escrow Does Not Lock Funds or Enforce Release Conditions<![CDATA[ ## Vulnerability Details **File Location**: `src/cli.ts:274-298, 319-373`; conflicting claims in `SKILL.md:17, 64-69, 95-111, 144` **Vulnerability Type**: Broken escrow design and misleading financial security behavior **Risk Level**: High ### Vulnerable Code Escrow creation only writes a local record: ```typescript const newEscrow: Escrow = { id: escrowId, amount: parseFloat(amount), sender: config.defaultWallet, recipient, condition: options.condition, status: 'pending', createdAt: new Date().toISOString() }; escrows.push(newEscrow); saveEscrows(escrows); ``` Release performs a new, ordinary wallet payment without condition enforcement: ```typescript console.log(`💸 Releasing ${escrow.amount} USDC to ${escrow.recipient}...`); try { const result = circleWallet(`send ${escrow.recipient} ${escrow.amount}`); console.log(result); escrow.status = 'released'; saveEscrows(escrows); console.log(`\n✅ Escrow ${id} released successfully!`); } catch (e: any) { console.error('❌ Release failed:', e.message); } ``` Refund only changes local state and performs no transfer: ```typescript escrow.status = 'refunded'; saveEscrows(escrows); console.log(`\n✅ Escrow ${id} refunded to sender.`); ``` The documentation advertises materially stronger behavior: ```markdown - 🤝 Escrow funds between agents for trustless transactions ``` ### Technical Analysis Creating an escrow does not transfer, reserve, or lock funds. It only stores mutable metadata in `escrows.json`. Consequently, the sender remains free to spend the supposed escrow funds before release. The release command does not release previously secured funds. It initiates a fresh payment from the currently configured wallet using the recipient and amount from the local record. No code verifies that the stated condition has been met, authenticates an independent approver, verifies an on-chain funding transaction, or confirms that the executing wallet is the recorded sender. Th ...[truncated 1761 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove or clearly disable the escrow commands until an actual escrow mechanism exists. - Revise all documentation so the current implementation is not described as escrow, funded escrow, or trustless. If retained, label it as an unauthenticated local payment reminder. - Implement escrow through an audited smart contract or supported custodial service that: - Transfers and locks funds during escrow creation. - Produces verifiable funding and transaction identifiers. - Enforces immutable state transitions. - Authenticates parties authorized to release or refund. - Prevents double release and replay. - Defines and enforces release conditions or an explicit arbitration mechanism. - Verifies chain, token contract, amount, sender, and recipient. - Bind release operations to the originally funded escrow rather than issuing a fresh payment from an arbitrary current wallet. - Verify the transaction result on-chain before changing local status. - Ensure refund performs and confirms an actual refund transaction before displaying success. - Treat local records as a cache of authoritative on-chain state, not as the source of truth. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
src/cli.ts:40
Finding
Predictable Escrow Identifiers and Insufficient Protection of Financial State Files<![CDATA[ ## Vulnerability Details **File Location**: `src/cli.ts:40-42, 58-61, 76-86` **Vulnerability Type**: Weak identifiers and insecure local state handling **Risk Level**: Medium ### Vulnerable Code Escrow and agent records are written without explicit restrictive file permissions: ```typescript function saveEscrows(escrows: Escrow[]): void { ensureDir(); fs.writeFileSync(ESCROWS_FILE, JSON.stringify(escrows, null, 2)); } ``` ```typescript function saveAgents(agents: Agent[]): void { ensureDir(); fs.writeFileSync(AGENTS_FILE, JSON.stringify(agents, null, 2)); } ``` Escrow identifiers contain only 10,000 possible values: ```typescript function generateEscrowId(): string { const num = Math.floor(Math.random() * 10000).toString().padStart(4, '0'); return `ESC-${num}`; } ``` For comparison, only the configuration file is explicitly restricted: ```typescript function saveConfig(config: Config): void { ensureDir(); fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); fs.chmodSync(CONFIG_FILE, 0o600); } ``` ### Technical Analysis `Math.random()` is not a cryptographically secure identifier generator, and the four-digit namespace permits only 10,000 escrow IDs. The implementation does not check whether a generated ID already exists before storing it. Release and refund locate records using the first matching identifier: ```typescript const escrow = escrows.find(e => e.id === id); ``` If duplicate IDs exist, operations may affect the wrong record. The escrow and agent files do not receive the explicit `0600` permissions applied to `config.json`. Their effective permissions therefore depend on the process umask and preexisting file state. The containing directory is also created without an explicit restrictive mode. Local modification is especially sensitive because escrow recipients and amounts later influence wallet payment execution. ### Attack Path 1. An escrow ID collides naturally due to the limited 10,000-value na ...[truncated 1253 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Generate escrow identifiers with a cryptographically secure, sufficiently large namespace: ```typescript import { randomUUID } from 'crypto'; function generateEscrowId(): string { return `ESC-${randomUUID()}`; } ``` - Check identifier uniqueness before adding a record, even when UUIDs are used. - Reject files containing duplicate IDs rather than selecting the first match. - Create the configuration directory with mode `0700`. - Create escrow, agent, and history files with mode `0600`; verify and correct permissions for preexisting files. - Use atomic writes through a securely created temporary file followed by a rename, preventing partial or corrupted state. - Validate the complete schema of every parsed JSON record, including identifier, address, amount, status, and timestamps. - Do not trust persisted recipient or amount values when authorizing a financial transaction. - For a genuine escrow implementation, derive authoritative state from authenticated on-chain or custodial records instead of mutable local JSON. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (20)

Intent-Code Divergence

High
Confidence
99% confidence
Finding
The escrow feature is presented as if it holds or locks funds, but creation only writes a local JSON record and does not transfer assets into any escrow account or smart contract. In a payments skill, this is dangerous because users or agents may rely on a false trust model and deliver goods/services without any funds actually being secured.

Intent-Code Divergence

High
Confidence
99% confidence
Finding
The refund command claims to refund the sender, but it only updates local status to 'refunded' and performs no payment action. This can mislead users into believing funds were returned, causing financial disputes or loss when the payment workflow is used as part of agent-to-agent transactions.

Context-Inappropriate Capability

High
Confidence
98% confidence
Finding
The helper wraps user-influenced strings into `execSync(`circle-wallet ${cmd}`)`, which invokes a shell. Multiple commands pass unsanitized CLI inputs into `cmd` such as API keys, wallet names, addresses, and amounts, so an attacker can inject shell metacharacters and execute arbitrary OS commands under the user's account. In a payments skill, this is especially dangerous because it runs in a high-trust financial workflow and can lead to wallet theft, secret exfiltration, or host compromise.

Intent-Code Divergence

High
Confidence
97% confidence
Finding
The `escrow create` flow only writes a local JSON record and never transfers funds into an escrow account or smart contract. Users are told an escrow was created, but no assets are locked, so the feature provides a false security guarantee and can cause counterparties to act on nonexistent payment assurances. In a payment/agent-deal context, misleading escrow semantics materially increase fraud and dispute risk.

Intent-Code Divergence

High
Confidence
99% confidence
Finding
The refund command marks an escrow as `refunded` in local storage and prints success, but performs no blockchain or wallet transfer. This can mislead users into believing funds were returned when no payment happened, potentially causing financial loss, incorrect accounting, and abuse in agent-to-agent transactions. Because the tool presents itself as handling money, false completion messages are especially risky.

Lp3

Medium
Category
MCP Least Privilege
Confidence
88% confidence
Finding
The skill exposes payment, wallet, escrow, and setup capabilities but does not declare any explicit tool scope or permissions despite requiring environment/credential access. In an agent ecosystem, missing scope boundaries can cause the host or user to grant broader capabilities than intended, increasing the chance of credential exposure or unauthorized financial actions.

Session Persistence

Medium
Category
Rogue Agent
Content
---
name: payclaw
version: 1.0.0
description: "Agent-to-Agent USDC payments. Create wallets, send/receive payments, escrow between agents. Built for the USDC Hackathon on Moltbook."
metadata: {"openclaw": {"emoji": "💸", "homepage": "https://github.com/rojasjuniore/payclaw"}}
---
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

Medium
Confidence
92% confidence
Finding
The setup instructions ask the user to provide a Circle API key but do not warn about secure handling, storage, rotation, or the risk of exposing credentials through shell history or logs. Because this skill controls wallets and payments, leaked credentials could enable unauthorized wallet management or fraudulent transactions.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The skill documents commands for sending payments and releasing escrow without a prominent warning that these actions may be irreversible and transfer funds. In a payment-focused skill, lack of explicit confirmation and risk messaging materially raises the chance of mistaken or socially engineered fund transfers.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
The wrapper executes `execSync` with a shell command string built from user-controlled input, and multiple call sites pass unsanitized values such as API keys, wallet names, addresses, and amounts into that string. This enables shell injection, allowing arbitrary command execution on the host running the CLI.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The API key is supplied as a CLI argument and then stored locally in config.json, which exposes it to shell history, process listings, and local file compromise. In a wallet/payment tool, credential exposure is especially sensitive because the key may enable unauthorized wallet operations or access to financial infrastructure.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The setup flow persists the Circle API key to a local JSON config file. Although file mode `0600` helps, storing long-lived payment credentials in plaintext without clear disclosure or stronger secret handling increases the risk of compromise from local malware, backups, accidental exposure, or multi-user environments. In a financial skill, API credentials are highly sensitive and deserve stronger protection.

Description-Behavior Mismatch

Medium
Confidence
90% confidence
Finding
The manifest description advertises both send and receive payments, but this file contains no mechanism to detect, accept, or process inbound payments from the network or wallet provider. The 'request' command only prints instructions, and 'history' displays local records rather than actual received transactions.

Vague Triggers

Low
Confidence
88% confidence
Finding
This is a manifest file, so vague-trigger checks apply. The description "Agent-to-Agent USDC Payments for OpenClaw" states a general capability but does not define specific activation conditions, scope constraints, or exclusion cases, which can make invocation criteria ambiguous in systems that rely on manifest metadata.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"author": "IntechChain",
  "license": "MIT",
  "dependencies": {
    "commander": "^12.1.0"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
Confidence
88% confidence
Finding
Using a caret range for the runtime dependency allows newer upstream versions to be installed automatically, which can introduce supply-chain risk, unexpected behavior changes, or malicious code if the dependency is compromised. Because this skill handles agent-to-agent USDC payments, dependency integrity matters more than in a non-financial tool.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"commander": "^12.1.0"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "ts-node": "^10.9.2",
    "typescript": "^5.9.3"
  }
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Unpinned Dependencies

Low
Category
Supply Chain
Content
},
  "devDependencies": {
    "@types/node": "^20.0.0",
    "ts-node": "^10.9.2",
    "typescript": "^5.9.3"
  }
}
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"devDependencies": {
    "@types/node": "^20.0.0",
    "ts-node": "^10.9.2",
    "typescript": "^5.9.3"
  }
}
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Missing User Warnings

Low
Confidence
84% confidence
Finding
After sending a payment, the code records recipient address, amount, memo, and timestamp into a local history.json file. This is a write of potentially sensitive financial metadata, but the command does not warn the user that transaction details will be stored locally.

Missing User Warnings

Low
Confidence
80% confidence
Finding
The register command stores agent identity metadata, including wallet address and optional description, in a local agents.json file. The command does not disclose that this information will be written to disk, which may matter for privacy-sensitive users.

Static analysis

Detected: suspicious.dangerous_exec

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
src/cli.ts:98