Back to skill

Security audit

ClankerHive

Security checks for vulnerabilities and agentic risk

Overview

This is a local SQLite coordination helper with no network behavior, but its shared database should not be used for sensitive data or strong locking guarantees.

Install only if you are comfortable with a persistent same-user SQLite store for coordination. Keep the database in a private per-user path, do not store secrets or sensitive personal data in facts or alerts, and do not rely on this version for security-sensitive locking or exactly-once workflow execution.

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

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/clankerhive.py:213
Finding

Non-Atomic Task Claiming Permits Duplicate Execution

Content
View full analysis

Vulnerability Details

File Location: scripts/clankerhive.py, lines 213–227
Vulnerability Type: Race condition in task deduplication
Risk Level: Medium

python
row = conn.execute(
    "SELECT status, owner FROM task_state WHERE task_id = ?", (args.task_id,)
).fetchone()
if row and row["status"] == "claimed":
    print(f"already-claimed by {row['owner']}")
    conn.close()
    sys.exit(1)
conn.execute(
    "INSERT OR REPLACE INTO task_state "
    "(task_id, status, owner, started_at, updated_at) VALUES (?, 'claimed', ?, ?, ?)",
    (args.task_id, owner, now, now),
)
conn.commit()
conn.close()
print("ok")

Technical Analysis

Task acquisition is implemented as a check-then-write sequence. The initial SELECT and subsequent INSERT OR REPLACE do not execute within a transaction that excludes competing writers for the entire operation.

Two processes can therefore read the same task state before either process commits its claim. If the task does not exist, or is not currently marked as claimed, both processes can proceed to the write. The use of INSERT OR REPLACE causes the later writer to replace the earlier claim instead of rejecting the conflicting acquisition.

SQLite serializes the individual writes, but that does not make the logical read-modify-write operation atomic. Consequently, more than one caller can receive ok and believe it exclusively owns the task.

Attack Path

  1. Two local sessions invoke claim-task concurrently with the same task ID.
  2. Both sessions execute the SELECT before either claim is committed.
  3. Both observe that no active claim prevents acquisition.
  4. The first session writes and commits its claim.
  5. The second session executes INSERT OR REPLACE, overwriting the first claim.
  6. Both sessions print ok and execute the supposedly deduplicated operation.
  7. Only the final writer remains recorded as the task owner, concealing the d ...[truncated 618 chars]
Remediation
View remediation

Remediation Suggestions

Replace the check-then-write sequence with a single atomic conditional operation. For tasks that must never be claimed twice, use an insertion that does not replace an existing row:

python
cur = conn.execute(
    "INSERT INTO task_state "
    "(task_id, status, owner, started_at, updated_at) "
    "VALUES (?, 'claimed', ?, ?, ?) "
    "ON CONFLICT(task_id) DO NOTHING",
    (args.task_id, owner, now, now),
)
conn.commit()

if cur.rowcount != 1:
    # Claim was not acquired.
    sys.exit(1)

If completed tasks are intentionally reclaimable, execute the state check and a conditional update inside a BEGIN IMMEDIATE transaction. The update should include the expected prior state in its WHERE clause, and success must be determined from the affected-row count.

Avoid INSERT OR REPLACE for claims because it can silently remove and recreate existing task state. Add a concurrency test that starts multiple processes against the same task ID and verifies that exactly one process reports successful acquisition.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/clankerhive.py:157
Finding

Non-Atomic Alert Claiming Allows Duplicate Delivery

Content
View full analysis

Vulnerability Details

File Location: scripts/clankerhive.py, lines 157–180
Vulnerability Type: Race condition in queue consumption
Risk Level: Medium

python
if args.topic:
    rows = conn.execute(
        "SELECT id, topic, payload, created_at FROM pending_alerts "
        "WHERE claimed_by IS NULL AND topic = ? ORDER BY id",
        (args.topic,),
    ).fetchall()
else:
    rows = conn.execute(
        "SELECT id, topic, payload, created_at FROM pending_alerts "
        "WHERE claimed_by IS NULL ORDER BY id"
    ).fetchall()

out = []
for r in rows:
    conn.execute(
        "UPDATE pending_alerts SET claimed_by = ?, claimed_at = ? WHERE id = ?",
        (claimer, now, r["id"]),
    )
    out.append(dict(r))

conn.commit()
conn.close()
print(json.dumps(out, indent=2))

Technical Analysis

The implementation first selects every unclaimed alert and then claims each selected row through separate UPDATE statements. The selection and updates are not protected as one exclusive transaction.

Concurrent consumers can therefore select the same rows while claimed_by is still NULL. In addition, the update only checks the alert ID; it does not require claimed_by IS NULL. A later consumer can overwrite claim metadata written by an earlier consumer, and every consumer appends the alert to its output regardless of whether it obtained an exclusive claim.

WAL mode improves concurrent database access but does not provide atomic queue-consumer semantics for this multi-statement sequence.

Attack Path

  1. At least one unclaimed alert exists in pending_alerts.
  2. Two sessions invoke pop-alerts concurrently, optionally using the same topic.
  3. Both sessions select the alert while claimed_by is NULL.
  4. The first session updates the alert with its claimant identity.
  5. The second session updates the same row because its statement only filters by ID.
  6. Both sess ...[truncated 743 chars]
Remediation
View remediation

Remediation Suggestions

Claim and retrieve alerts as one atomic database operation. Where supported by the deployed SQLite version, use an atomic conditional update with RETURNING. Alternatively:

  1. Start a BEGIN IMMEDIATE transaction before selecting alerts.
  2. Select the eligible unclaimed rows.
  3. Update each row with a condition that preserves exclusivity:
    sql
    UPDATE pending_alerts
    SET claimed_by = ?, claimed_at = ?
    WHERE id = ? AND claimed_by IS NULL
    
  4. Add an alert to the returned output only when the update affects exactly one row.
  5. Commit only after all claims and returned results have been finalized.

Consider using a unique, unpredictable claim token rather than a caller-supplied identity for subsequent acknowledgement operations. Add multiprocess tests confirming that an alert is returned to exactly one consumer under concurrent calls.

T05 · Unauthorized Access and Privilege Escalation

Note
Location
scripts/clankerhive.py:231
Finding

Task Completion Does Not Verify the Claim Owner

Content
View full analysis

Vulnerability Details

File Location: scripts/clankerhive.py, lines 231–246
Vulnerability Type: Missing authorization check for task-state transitions
Risk Level: Low

python
def cmd_release_task(args: argparse.Namespace) -> None:
    """Mark a claimed task as done. Exits 1 if task not found."""
    conn: sqlite3.Connection = _db()
    now: int = int(time.time())
    result: str = args.result or ""
    cur = conn.execute(
        "UPDATE task_state SET status = 'done', updated_at = ?, result = ? WHERE task_id = ?",
        (now, result, args.task_id),
    )
    conn.commit()
    conn.close()
    if cur.rowcount:
        print("ok")
    else:
        print("not-found")
        sys.exit(1)

Technical Analysis

Task claims record an owner, but release-task neither accepts nor verifies that owner. The state transition is authorized solely by knowledge of the task ID. The update also does not require the task to remain in the claimed state.

As a result, any process with access to the same database and command-line tool can mark another process's task as complete and supply an arbitrary result. The existing --owner value is caller-controlled and is not an authenticated identity, so merely comparing caller-provided owner strings would not provide robust protection.

Attack Path

  1. A legitimate session claims a task and begins performing the protected operation.
  2. Another local session learns or predicts the task ID.
  3. The second session invokes release-task TASK_ID --result "done" without possessing proof of the claim.
  4. The unconditional update marks the task as done and stores the supplied result.
  5. Other sessions checking the task state may skip the required work or trust the falsified result.
  6. The legitimate claimant can no longer rely on the recorded state to represent its actual completion.

Impact Assessment

A competing or compromised same-us ...[truncated 606 chars]

Remediation
View remediation

Remediation Suggestions

Generate a cryptographically random claim token when a task is acquired, store only that claim credential or an appropriate protected representation, and return the token to the successful claimant. Require the token when releasing the task:

sql
UPDATE task_state
SET status = 'done', updated_at = ?, result = ?
WHERE task_id = ?
  AND status = 'claimed'
  AND claim_token = ?

Treat an affected-row count of zero as an authorization or stale-state failure. Do not rely only on caller-supplied owner names because another local caller can reproduce them.

Where different operating-system users require isolation, place the database in a directory with restrictive permissions and avoid sharing it across trust boundaries. Document that possession of database write access otherwise permits direct state modification. Add tests proving that a task cannot be completed using a missing, invalid, expired, or unrelated claim token.

Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Output HandlingUnvalidated Output Injection, Cross-Context Output, Unbounded Output
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
Findings (9)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding

The skill documents shell execution and environment-variable use but does not declare any tool scope or permissions metadata. This can cause an agent framework to invoke the skill without clear sandboxing or user-visible capability boundaries, increasing the chance of unintended command execution or environment access.

Content

No source excerpt is available for this finding.

Cross-Context Output

Medium
Category
Output Handling
Confidence
95% confidence
Finding

The skill is explicitly designed for cross-session state sharing and alert handoff, which creates a cross-context output channel. Without strict namespacing, provenance, and access controls, one session can influence another, enabling data leakage, spoofed alerts, or unsafe action based on untrusted stored state.

Content

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

md
---
name: clankerhive
description: "Shared SQLite-backed context store for multi-session agent coordination. Use when: (1) checking if work was already done recently (email checked, briefing sent), (2) preventing duplicate cron/heartbeat runs via task claiming, (3) passing alerts between sessions (cron queues alert → main session pops it), (4) storing short-lived facts with TTL, or (5) any cross-session state sharing. Replaces ad-hoc JSON state files with a proper coordination bus. Triggers on: deduplication, cross-session state, shared facts, alert queue, task coordination, heartbeat state."
homepage: https://github.com/pfrederiksen/clankerhive
metadata:
  {

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The trigger language is broad and ambiguous, including generic phrases like cross-session state, shared facts, and task coordination. Over-broad activation can cause the skill to run in unintended contexts, which is especially risky because it persists and shares data across sessions.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
90% confidence
Finding

The skill introduces durable session persistence for coordination data, replacing ad-hoc state files with a shared database. Persistence across sessions can be dangerous because stale, spoofed, or sensitive state may survive longer than intended and affect future agent decisions.

Content

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

Instead of maintaining a separate memory/heartbeat-state.json file, use ClankerHive facts:

bash
# Old way: read/write JSON file
# New way:
python3 scripts/clankerhive.py set heartbeat.email "$(date +%s)" --ttl 1800
python3 scripts/clankerhive.py set heartbeat.calendar "$(date +%s)" --ttl 3600

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
80% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

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

md
**Reads:** Nothing — no files, env vars, or network beyond the SQLite DB.

**Writes:** Only to the SQLite database file at `$CLANKERHIVE_DB` (default `~/.openclaw/hive.db`). Creates parent directories if they don't exist. The default path (`~/.openclaw/`) is typically mode `700` on OpenClaw installs, meaning only the owning user can read the DB. If you change `CLANKERHIVE_DB` to a shared or world-readable location, restrict permissions manually: `chmod 600 hive.db`.

**Network:** None. No outbound connections of any kind.

Unsafe Defaults

Medium
Category
Tool Misuse
Confidence
74% confidence
Finding

The documentation permits overriding the database path via CLANKERHIVE_DB and only warns users to restrict permissions if they choose a shared or world-readable location. Because this skill is a cross-session coordination bus, storing its database in an insecure location could expose or allow tampering with alerts, facts, and task claims.

Content

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

md
**Reads:** Nothing — no files, env vars, or network beyond the SQLite DB.

**Writes:** Only to the SQLite database file at `$CLANKERHIVE_DB` (default `~/.openclaw/hive.db`). Creates parent directories if they don't exist. The default path (`~/.openclaw/`) is typically mode `700` on OpenClaw installs, meaning only the owning user can read the DB. If you change `CLANKERHIVE_DB` to a shared or world-readable location, restrict permissions manually: `chmod 600 hive.db`.

**Network:** None. No outbound connections of any kind.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
99% confidence
Finding

The System Access section states the skill reads no environment variables, but the skill explicitly depends on CLANKERHIVE_DB. Misstating data access weakens operator trust and may lead reviewers to underestimate where the skill can read configuration from or how runtime behavior can be altered.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
83% confidence
Finding

The delete command removes a stored fact and commits the change immediately, but provides no confirmation prompt or explicit warning that the action is destructive. While the docstring says 'Delete a fact by key,' it does not clearly disclose irreversibility or provide any guard against accidental removal.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
86% confidence
Finding

The purge-alerts command permanently deletes claimed alerts from the SQLite store, but the implementation only performs the deletion and prints a post-action count. There is no confirmation prompt or stronger user-facing warning at execution time beyond the brief help text, despite this being an irreversible data-removal operation.

Content

No source excerpt is available for this finding.