Back to skill

Security audit

Agent Mesh

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent P2P agent-messaging tool, but it needs Review because it can feed remote peer messages into an agent control hook and runs as a persistent internet-connected daemon.

Install only if you are comfortable running a persistent internet-connected P2P daemon for agent messages. Treat all peer messages as untrusted, avoid the Claude Stop hook until it stops injecting message bodies into hook reasons, do not send secrets, pin dependencies before use, and review ~/.decent-peer permissions, logs, retention, allow.txt entries, and any watchdog or startup configuration.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • 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
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
Findings (5)

T01 · Skill Instruction Hijacking

Error
Location
stop-hook.sh:25
Finding

Remote Prompt Injection Through the Claude Stop Hook

Content
View full analysis
/dev/null || true) case "$msgs" in ""|*"inbox empty"*) exit 0 ;; esac # block the stop and hand the message content back to Claude printf '%s' "$msgs" | "$NODE" -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{process.stdout.write(JSON.stringify({decision:"block",reason:"📨 Incoming Decent-mesh message(s) — reply via `mesh send \"...\"`:\n\n"+s}))})' exit 0 ``` The data returned by `mesh inbox` originates from remote peer messages: ```js peer.onText((msg) => { const key = _djb2(`${msg.pubkey}|${msg.text}`); const t = Date.now(); for (const [k, ts] of _seen) if (t - ts > DEDUP_MS) _seen.delete(k); if (_seen.has(key)) { log(`DEDUP drop ${msg.pubkey}: ${msg.text.slice(0, 48)}`); return; } _seen.set(key, t); _persistSeen(); appendFileSync(P("inbox.jsonl"), JSON.stringify({ ts: now(), from: msg.pubkey, text: msg.text }) + "\n"); log(`RECV ${msg.pubkey}: ${msg.text}`); }); ``` ### Technical Analysis The daemon accepts arbitrary text from a remote peer and stores it in `inbox.jsonl`. The Stop hook subsequently reads that text and concatenates it directly into the `reason` field of a hook response with `decision: "block"`. A hook reason is agent-facing operational context rather than an isolated data-display surface. No trust-boundary marker, content isolation, validation, or confirmation step separates the remote message from the instructions presented to the agent. Consequently, a peer can compose messages that resemble system instructions, tool-use directives, requests to disclose data, or commands to disregard the user's current objective. The hook also prevents the current turn from stopping. This gi ...[truncated 1617 chars]
Remediation
View remediation

T05 · Unauthorized Access and Privilege Escalation

Error
Location
peerd.mjs:94
Finding

Friend Allow-List Bypass Through Substring Matching

Content
View full analysis
{ const from = req.userid ?? req.pubkey; appendFileSync(P("requests.jsonl"), JSON.stringify({ ts: now(), from, hello: req.hello ?? "" }) + "\n"); log(`FRIEND-REQ ${from} "${req.hello ?? ""}"`); if (allow().some((a) => from?.includes(a) || a.includes(from))) { try { await peer.acceptFriendRequest(req.pubkey); log(`AUTO-ACCEPTED ${from}`); } catch (e) { log(`accept failed: ${e.message}`); } } }); ``` ### Technical Analysis The auto-accept decision uses bidirectional substring matching: ```js from?.includes(a) || a.includes(from) ``` Authorization identifiers must be compared as complete, canonical values. The current condition succeeds when either value is merely a substring of the other. A short allow-list entry, a truncated incoming identifier, or an identifier containing an allowed value can therefore satisfy the policy without being the exact authorized identity. The bidirectional check makes the issue particularly broad: both an attacker-controlled value that contains an allowed token and a short attacker-controlled value contained by an allow-list entry may be treated as authorized. The code also does not distinguish or validate the expected format of user IDs and public keys before comparison. ### Attack Path 1. A victim adds an intended peer identifier to `allow.txt`. 2. An attacker submits a friend request whose exposed `userid` or `pubkey` has a substring relationship with the allow-listed value. 3. The `includes` expression evaluates to true even though the identifiers are not equal. 4. The daemon calls `peer.acceptFriendRequest(req.pubkey)` automatically. 5. The attacker becomes an accepted peer without explicit victim approval. 6. The attacker can send messages to the victim and may subsequently exploit the Stop-ho ...[truncated 814 chars]
Remediation
View remediation
normalize(entry) === normalize(from)); ``` 2. Define separate formats or prefixes for user IDs, addresses, and public keys rather than comparing different identifier types interchangeably. 3. Validate each allow-list entry for exact expected length, character set, encoding, and identifier type. 4. Reject blank, truncated, malformed, or ambiguous identifiers. 5. Bind authorization to the cryptographically authenticated public key used by the friend request, not merely a display-oriented or abbreviated user ID. 6. Require manual confirmation when an incoming request does not exactly match one full authorized key. 7. Log rejected near-matches as security events without automatically accepting them. 8. Add tests proving that prefixes, suffixes, and contained substrings never satisfy the allow-list policy. ]]>

T08 · Insecure Dependencies

Warning
Location
package.json:33
Finding

Runtime Dependency Is Not Reproducibly Pinned

Content
View full analysis
&& npm install ``` (pulls `@decentnetwork/peer` from npm.) ``` No dependency lockfile is included in the audited project structure. ### Technical Analysis The caret range `^0.1.112` allows npm to resolve later compatible releases rather than the exact version represented by the audit expectations. Without a committed lockfile and integrity metadata, separate installations can retrieve different package contents. The dependency is imported directly into the long-running daemon: ```js ({ Peer } = await import("@decentnetwork/peer")); ``` It therefore executes under the same user account and has access to the daemon's identity file, message storage, environment variables, filesystem permissions, and network connectivity. This finding does not establish that the named package is currently malicious; it identifies an unsafe and non-reproducible supply-chain configuration. ### Attack Path 1. A future package version within the accepted semver range is compromised, malicious, or contains an exploitable regression. 2. A user follows the documentation and runs `npm install`. 3. npm resolves and installs the newer accepted version because no lockfile fixes the dependency graph. 4. `peerd.mjs` imports and executes the installed package. 5. The compromised dependency runs with the permissions of the daemon account and can access peer identity and message data. ### Impact Assessment A compromised runtime dependency could potentially: - Read or replace `peer.save` and impersonate the peer identity. - Rea ...[truncated 407 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
peerd.mjs:85
Finding

Plaintext Message Persistence and Logging Without Explicit Restrictive Permissions

Content
View full analysis
{ const key = _djb2(`${msg.pubkey}|${msg.text}`); const t = Date.now(); for (const [k, ts] of _seen) if (t - ts > DEDUP_MS) _seen.delete(k); if (_seen.has(key)) { log(`DEDUP drop ${msg.pubkey}: ${msg.text.slice(0, 48)}`); return; } _seen.set(key, t); _persistSeen(); appendFileSync(P("inbox.jsonl"), JSON.stringify({ ts: now(), from: msg.pubkey, text: msg.text }) + "\n"); log(`RECV ${msg.pubkey}: ${msg.text}`); }); ``` Sent messages are also logged in full: ```js for (; cursor < lines.length; cursor++) { let cmd; try { cmd = JSON.parse(lines[cursor]); } catch { continue; } try { if (cmd.type === "send") { await peer.sendText(cmd.to, cmd.text); log(`SENT ${cmd.to}: ${cmd.text}`); } else if (cmd.type === "friend") { await peer.sendFriendRequest(cmd.to, cmd.hello ?? `hello from ${NAME}`); log(`FRIEND-REQ -> ${cmd.to}`); } else if (cmd.type === "accept") { await peer.acceptFriendRequest(cmd.to); log(`ACCEPTED ${cmd.to}`); } ``` ### Technical Analysis The application stores complete message bodies in plaintext JSONL files and writes the same data to standard output, which the documentation redirects to `~/.decent-peer/peerd.log`. Neither directory creation nor file writes specify restrictive permission modes. The actual permissions are consequently dependent on the caller's umask and any pre-existing directory or file permissions. On systems with permissive configuration, other local users or processes may be able to read peer identity m ...[truncated 1440 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
heartbeat_watchdog.sh:17
Finding

Watchdog Uses an Overbroad Process-Matching Kill Command

Content
View full analysis
/dev/null 2>&1 && pm2 describe peerd >/dev/null 2>&1; then pm2 restart peerd >/dev/null 2>&1 else pkill -f peerd.mjs 2>/dev/null; sleep 3 cd "$HOME/.claude/skills/agent-mesh" && nohup node peerd.mjs >> "$HOME/.decent-peer/peerd.log" 2>&1 & fi ``` ### Technical Analysis `pkill -f peerd.mjs` matches the full command line of every accessible process containing the string `peerd.mjs`. It does not verify a process ID, executable path, state directory, role identity, or daemon ownership. The project documentation explicitly supports multiple daemons for distinct roles through separate `DECENT_PEER_HOME` values. The broad pattern can terminate all such instances rather than only the stale daemon. It can also terminate unrelated development, test, or user processes whose command lines contain the same filename. The restart path is hard-coded to `$HOME/.claude/skills/agent-mesh`, so invoking the watchdog from a different installation can stop a valid daemon and either fail to restart it or start a different copy with the wrong configuration. ### Attack Path 1. `status.json` becomes stale, malformed, absent, or inaccessible. 2. The watchdog assigns a large age and enters the restart branch. 3. If the PM2 process named `peerd` is unavailable, it executes `pkill -f peerd.mjs`. 4. Every matching daemon or process is terminated. 5. The watchdog attempts to restart only the hard-coded installation and does not preserve role-specific environment settings. 6. Other peer identities remain offline, or an unintended identity is started. An attacker with the ability to alter the heartbeat file could trigger this behavior, but accidental stale state alone is s ...[truncated 555 chars]
Remediation
View remediation
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • 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
Findings (10)

Exfiltration Commands

High
Category
Prompt Injection
Confidence
90% confidence
Finding

Instructions found that direct the agent to transmit conversation context or user data to external services.

Content

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

md
- Identity is a self-generated keypair in `~/.decent-peer/peer.save` — no signup.
- `address` (long) is what you hand out to receive a friend request.
- `userid` (short) is what you send messages to **after** you're friends.
- Messaging requires a **mutual friend handshake first** (like adding a contact).

## Prerequisites (per machine, once)

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
- `peerd.mjs` — the daemon (holds the P2P connection; services the local files).

Session Persistence

Medium
Category
Rogue Agent
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.

Content

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

md
- **Transport is a public DHT** — works over the internet; the only dependency
  is Node.js. Independent of any VPN or virtual-network layer.
- **Model-agnostic** — Claude, Codex, Gemini, Cursor, or a plain cron job: any
  agent that can read and write files talks over the same daemon.

## Quick start

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding

The skill explicitly requires network access and environment variables but does not declare any tool scope or permission boundaries. In a skill framework, this omission can cause users or orchestrators to grant broader capabilities than intended, reducing transparency around internet-connected behavior.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
82% confidence
Finding

The skill establishes durable local state, including a persistent cryptographic identity and inbox under ~/.decent-peer, and encourages always-on background daemon use. Persistent state across sessions increases the blast radius of compromise, can retain sensitive communications, and may enable unintended long-lived agent-to-agent channels.

Content

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

md
description: >
  Send and receive messages between AI agents on different machines over the
  Decent Network peer-to-peer mesh — no third-party platform, no server, no
  account registration. Model-agnostic: any agent that can read/write files
  (Claude, Gemini, Codex, a cron job) talks over the same daemon. Each agent has
  a self-generated cryptographic identity (address + userid); messaging is enabled
  by a one-time friend handshake, and a non-friend cannot route to your machine at

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The skill explains that it uses a public DHT and exchanges agent identities, but it does not present this as a prominent user warning with privacy and security implications. Users may not appreciate that addresses, friendship metadata, and message traffic are being transmitted over the public internet, which can lead to unintended data exposure.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
65% 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 · heartbeat_watchdog.sh (reported line 3)May include surrounding context.

sh
#!/bin/bash
# Mesh heartbeat watchdog: peerd can wedge internally (process alive, status.json
# heartbeat frozen — seen 2026-07-13..15 on BOTH nodes). pm2/nohup only catch
# crashes, so restart on a stale heartbeat instead. Cron: every 5 min.
S="$HOME/.decent-peer/status.json"
AGE=$(python3 - <<'PY'

Session Persistence

Medium
Category
Rogue Agent
Confidence
65% 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 · heartbeat_watchdog.sh (reported line 23)May include surrounding context.

sh
#!/bin/bash
# Mesh heartbeat watchdog: peerd can wedge internally (process alive, status.json
# heartbeat frozen — seen 2026-07-13..15 on BOTH nodes). pm2/nohup only catch
# crashes, so restart on a stale heartbeat instead. Cron: every 5 min.
S="$HOME/.decent-peer/status.json"
AGE=$(python3 - <<'PY'

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
94% confidence
Finding

The header materially misrepresents the safety properties of the CLI by claiming it is 'safe to call from any Claude turn' and 'never touches the network itself,' even though commands like send, friend, and accept enqueue actions that trigger real outbound communication through the daemon. In an agent skill context, misleading safety claims can cause an autonomous model or operator to invoke the tool in situations where network egress, identity disclosure, or unsolicited contact should require explicit approval.

Content

No source excerpt is available for this finding.

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
89% confidence
Finding

The dependency uses a caret range (^0.1.112), allowing npm to install newer compatible releases within the 0.1.x series. If the upstream package is compromised or a bad release is published, installs can silently pull different code than was reviewed, which is especially sensitive here because this skill is a network-facing P2P messaging daemon that depends on cryptographic identity and peer communication behavior.

Content

Scanner excerpt · package.json (reported line 34)May include surrounding context.

json
"mesh"
  ],
  "dependencies": {
    "@decentnetwork/peer": "^0.1.112"
  },
  "engines": {
    "node": ">=20"

Static analysis

No suspicious patterns detected.