Back to skill

Security audit

X Bookmark Triage

Security checks for vulnerabilities and agentic risk

Overview

The skill is purpose-aligned but needs review because it can automatically remove X bookmarks and has unsafe token-handling and failure-ordering issues.

Review before installing. Use read-only X scopes or pass --no-unbookmark until deletion is opt-in and guarded by confirmed Discord delivery. Store OAuth tokens carefully, restrict data file permissions, and avoid running the .env-sourcing wrapper against any workspace you do not fully trust.

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 (2)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/bookmark-poll.js:33
Finding

OAuth token cache may be created with overly permissive filesystem permissions

Content
View full analysis

Vulnerability Details

File Location: scripts/bookmark-poll.js:33-38; duplicate vulnerable write at scripts/backlog-sweep.js:94-95
Vulnerability Type: Sensitive credential storage with permissions inherited from the process umask
Risk Level: High

Vulnerable Code

scripts/bookmark-poll.js:33-38:

js
function saveTokenCache(data) {
  fs.mkdirSync(path.dirname(TOKEN_CACHE), { recursive: true });
  fs.writeFileSync(TOKEN_CACHE, JSON.stringify({
    ...data,
    cached_at: Date.now()
  }, null, 2));
}

scripts/backlog-sweep.js:94-95:

js
fs.mkdirSync(path.dirname(TOKEN_CACHE), { recursive: true });
fs.writeFileSync(TOKEN_CACHE, JSON.stringify({ ...data, cached_at: Date.now() }, null, 2));

Technical Analysis

Both scripts persist the complete OAuth token response to data/x-oauth2-token-cache.json without specifying a restrictive file mode. The response contains an X access token and may also contain a refresh token. When the cache file is first created, its permissions are determined by the process umask. Under a common 022 umask, the resulting file may be readable by other local users.

This behavior contradicts the security claim in README.md that OAuth tokens are stored with mode 0o600. Although scripts/x-oauth2-authorize.js and the rotated-token write explicitly use 0o600, subsequent cache creation in these two polling scripts does not provide the same protection.

If the file already exists with insecure permissions, adding a mode only to later writes is insufficient because opening an existing file does not necessarily replace its mode. Existing cache files must also be explicitly repaired.

Attack Path

  1. A user configures valid X OAuth credentials and runs bookmark-poll.js or backlog-sweep.js.
  2. No token cache currently exists, so the script creates data/x-oauth2-token-cache.json.
  3. The process uses a permissive umask, causing the file t ...[truncated 1112 chars]
Remediation
View remediation

Remediation Suggestions

  1. Create the data directory with owner-only permissions and explicitly set token files to 0o600:

    js
    function saveTokenCache(data) {
      fs.mkdirSync(path.dirname(TOKEN_CACHE), {
        recursive: true,
        mode: 0o700
      });
    
      fs.writeFileSync(
        TOKEN_CACHE,
        JSON.stringify({ ...data, cached_at: Date.now() }, null, 2),
        { mode: 0o600 }
      );
    
      fs.chmodSync(TOKEN_CACHE, 0o600);
    }
    
  2. Apply the same correction in backlog-sweep.js.

  3. Repair existing installations during startup by checking and resetting the cache mode with fs.chmodSync.

  4. Prefer atomic credential writes: write to a randomly named owner-only file in the same directory, call fsync, set mode 0o600, and rename it over the destination.

  5. Store refresh tokens in an operating-system credential store or dedicated secrets manager where practical.

  6. Add a setup check that rejects or warns about token files readable by group or other users.

  7. Update documentation so its 0o600 claim is verified by all token-writing paths.

T09 · Insecure Skill Coding Practices

Error
Location
scripts/triage-url.js:340
Finding

Discord delivery failure is treated as successful and can cause permanent bookmark deletion

Content
View full analysis

Vulnerability Details

File Location: scripts/triage-url.js:340-345, with destructive callers at scripts/bookmark-poll.js:236-245 and scripts/backlog-sweep.js:257-263
Vulnerability Type: Incorrect success signaling and unsafe ordering of state-changing operations
Risk Level: High

Vulnerable Code

scripts/triage-url.js:340-345 marks the URL as processed before confirming delivery:

js
const card = buildCard(triage, fetched, url);

// Mark as seen BEFORE Discord post — ensures durability even if post fails
markSeen(url);

const posted = await postToDiscord(card);

The script only emits an optional event when posted is true. When posted is false, it reaches the end of the asynchronous entry point without setting a nonzero exit code. Node.js therefore normally exits with status zero.

scripts/bookmark-poll.js:236-245 interprets that zero exit status as successful triage and removes the source bookmark:

js
} else if (result.status === 0) {
  triaged++;
  // Unbookmark after successful triage
  if (bookmark.tweetId && userId) {
    const deleted = deleteBookmark(accessToken, userId, bookmark.tweetId);
    if (deleted) {
      unbookmarked++;
      console.log(`[bookmark-poll] 🗑️  Unbookmarked: ${bookmark.tweetId}`);
    } else {
      console.warn(`[bookmark-poll] ⚠️  Unbookmark failed for: ${bookmark.tweetId}`);
    }
  }

scripts/backlog-sweep.js:257-263 contains equivalent behavior:

js
} else if (result.status === 0) {
  totalTriaged++;
  if (!NO_UNBOOKMARK && bookmark.tweetId) {
    if (deleteBookmark(accessToken, userId, bookmark.tweetId)) {
      totalUnbookmarked++;
      console.log(`[backlog-sweep] 🗑️  Unbookmarked: ${bookmark.tweetId}`);
    }
  }
}

Technical Analysis

The pipeline uses child-process exit status as its success contract. However, triage-url.js does not make confirmed Discord delivery a condit ...[truncated 2329 chars]

Remediation
View remediation

Remediation Suggestions

  1. Treat confirmed Discord delivery as part of the success condition:

    js
    const card = buildCard(triage, fetched, url);
    const posted = await postToDiscord(card);
    
    if (!posted) {
      console.error('[triage-url] Discord delivery failed');
      process.exitCode = 1;
      return;
    }
    
    markSeen(url);
    
  2. Mark a URL as seen only after Discord returns a valid message ID.

  3. Return a machine-readable result from triage-url.js, such as:

    json
    {
      "triaged": true,
      "posted": true,
      "message_id": "123",
      "safe_to_unbookmark": true
    }
    

    Parent scripts should delete a bookmark only when safe_to_unbookmark is explicitly true rather than relying solely on exit status or matching console text.

  4. Separate delivery states such as pending, posted, and failed. Persist pending work in a retry queue rather than treating it as seen.

  5. Preserve the source bookmark whenever classification or delivery fails.

  6. For low-tier items intentionally suppressed from Discord, require an explicit configuration option before deleting them; successful classification alone should not silently imply successful capture.

  7. Make unbookmarking opt-in by default or use read-only OAuth without bookmark.write unless automatic removal is explicitly enabled.

  8. Add tests covering invalid Discord tokens, inaccessible channels, network failures, rate-limit exhaustion, malformed API responses, and process timeouts. Each test should verify that the X deletion endpoint is not invoked.

Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (104)

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding

The skill description does not clearly disclose that it performs an interactive OAuth flow with a localhost callback server, browser launch, and token caching. Those behaviors materially expand the trust boundary because they create local listeners and store long-lived credentials on disk.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The skill description does not clearly disclose that it performs an interactive OAuth flow with a localhost callback server, browser launch, and token caching. Those behaviors materially expand the trust boundary because they create local listeners and store long-lived credentials on disk.

Content

No source excerpt is available for this finding.

Tool Parameter Abuse

High
Category
Tool Misuse
Confidence
95% confidence
Finding

The skill performs DELETE /bookmarks/:id, which is a destructive API action against the user's X account. In context, this is more dangerous because the deletion is tied to automated polling and dedup logic, so accidental invocation or misclassification can silently remove a user's bookmark backlog at scale.

Content

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

Discord #knowledge-intake (structured card) │ ▼ X API DELETE /bookmarks/:id (unbookmark)

text

## Prerequisites

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
t env var `KNOWLEDGE_INTAKE_CHANNEL_ID=<your-channel-id>` (preferred), OR edit `scripts/triage-url.js` line 19 to hardcode it.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
t env var `KNOWLEDGE_INTAKE_CHANNEL_ID=<your-channel-id>` (preferred), OR edit `scripts/triage-url.js` line 19 to hardcode it.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
t env var `KNOWLEDGE_INTAKE_CHANNEL_ID=<your-channel-id>` (preferred), OR edit `scripts/triage-url.js` line 19 to hardcode it.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
t env var `KNOWLEDGE_INTAKE_CHANNEL_ID=<your-channel-id>` (preferred), OR edit `scripts/triage-url.js` line 19 to hardcode it.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
t env var `KNOWLEDGE_INTAKE_CHANNEL_ID=<your-channel-id>` (preferred), OR edit `scripts/triage-url.js` line 19 to hardcode it.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
t env var `KNOWLEDGE_INTAKE_CHANNEL_ID=<your-channel-id>` (preferred), OR edit `scripts/triage-url.js` line 19 to hardcode it.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
t env var `KNOWLEDGE_INTAKE_CHANNEL_ID=<your-channel-id>` (preferred), OR edit `scripts/triage-url.js` line 19 to hardcode it.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
# You can also run manually: node scripts/bookmark-poll.js

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
# You can also run manually: node scripts/bookmark-poll.js

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
# You can also run manually: node scripts/bookmark-poll.js

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
node scripts/backlog-sweep.js --delay 3

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
node scripts/backlog-sweep.js --delay 3

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
node scripts/backlog-sweep.js --delay 3

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/backlog-sweep.js (reported line 26)May include surrounding context.

js
const WORKSPACE = process.env.OPENCLAW_WORKSPACE || path.resolve(__dirname, '../../..');
const TOKEN_CACHE = path.join(WORKSPACE, 'data/x-oauth2-token-cache.json');
const SEEN_FILE = path.join(WORKSPACE, 'data/knowledge-intake-seen.json');
const SECRETS_FILE = process.env.X_OAUTH2_SECRETS_FILE || path.join(WORKSPACE, '..', '..', 'secrets', 'x-oauth2-credentials.json');

// Load refresh token from secrets file if not in env
// client_id + client_secret must come from plist env (or be passed in)

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/bookmark-poll.js (reported line 53)May include surrounding context.

js
// Check cache — tokens last 2 hours, use if <90 min old
  const cache = loadTokenCache();
  if (cache?.access_token && (Date.now() - cache.cached_at) < 90 * 60 * 1000) {
    console.log('[bookmark-poll] Using cached access token');
    return cache.access_token;
  }

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/bookmark-poll.js (reported line 57)May include surrounding context.

js
// Check cache — tokens last 2 hours, use if <90 min old
  const cache = loadTokenCache();
  if (cache?.access_token && (Date.now() - cache.cached_at) < 90 * 60 * 1000) {
    console.log('[bookmark-poll] Using cached access token');
    return cache.access_token;
  }

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

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

md
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Load credentials from .env file if it exists (non-OpenClaw users)
ENV_FILE="${OPENCLAW_WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}/.env"
if [ -f "$ENV_FILE" ]; then
  set -a

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · references/adapting.md (reported line 56)May include surrounding context.

md
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Load credentials from .env file if it exists (non-OpenClaw users)
ENV_FILE="${OPENCLAW_WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}/.env"
if [ -f "$ENV_FILE" ]; then
  set -a

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/run-poll.sh (reported line 7)May include surrounding context.

sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Load credentials from .env file if it exists (non-OpenClaw users)
ENV_FILE="${OPENCLAW_WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}/.env"
if [ -f "$ENV_FILE" ]; then
  set -a

Credential Access

High
Category
Privilege Escalation
Confidence
90% confidence
Finding

The script uses 'source' on a .env file, which executes the file as shell code rather than parsing it as data. If an attacker can modify that .env file or influence OPENCLAW_WORKSPACE to point at a malicious file, they can achieve arbitrary command execution when the scheduled wrapper runs, potentially exposing all available secrets and the user's account context.

Content

Scanner excerpt · scripts/run-poll.sh (reported line 8)May include surrounding context.

sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Load credentials from .env file if it exists (non-OpenClaw users)
ENV_FILE="${OPENCLAW_WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}/.env"
if [ -f "$ENV_FILE" ]; then
  set -a
  source "$ENV_FILE"

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/run-poll.sh (reported line 13)May include surrounding context.

sh
set -a
  source "$ENV_FILE"
  set +a
  echo "[run-poll] Loaded credentials from .env"
fi

# Load from OpenClaw gateway plist if present (OpenClaw users)

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The README prominently advertises automatic unbookmarking as part of normal operation but does not provide a strong warning that this modifies user data and may be irreversible in practice. In an automation/agent setting, this increases the risk of accidental loss of bookmark state if the skill is enabled or triggered without the user fully understanding the consequences.

Content

No source excerpt is available for this finding.

Static analysis

Detected: suspicious.dangerous_exec, suspicious.exposed_secret_literal

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/backlog-sweep.js:70

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/bookmark-poll.js:65

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/poll-channel.js:48

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/triage-url.js:72

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
scripts/x-oauth2-authorize.js:138