Back to skill

Security audit

session-fork

Security checks for vulnerabilities and agentic risk

Overview

This session-copying tool needs review because repair mode can alter original conversations despite a preview flag, and some copies can lose important history.

Review before using this version on important conversations. Avoid WorkBuddy --fix, including with --dry-run, until its safeguards are corrected. Back up application session data independently and select an explicit source session ID. Check copied history before resuming work: referenced Codex history and malformed entries may be omitted despite successful verification. Direct inspection addressed the scanner's material gaps, but live application compatibility remains unverified.

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

T09 · Insecure Skill Coding Practices

Warning
Location
fork_core/cli.py
Finding

Repair mode ignores --dry-run and truncates transcripts without confirmation

Content
View full analysis

Impact

Trailing user messages, tool records, and unfinished-turn entries are removed from the live transcript despite --dry-run. Because branch membership is not checked, an original conversation can also be modified.

Vulnerability Patterns
  • Output HandlingUnvalidated Output Injection, Cross-Context Output, Unbounded Output
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (16)

Code capability 'env' detected in fork_core/adapter_base.py but not covered by declared permissions.

High
Category
MCP Least Privilege
Confidence
75% confidence
Finding

The skill uses 'env' capability that is not covered by the declared 'allowed-tools' preapproval. Review the host runtime's actual enforcement policy; runtime permissions are unknown unless separately verified, and this declaration alone does not establish a permission ceiling.

Content

No source excerpt is available for this finding.

Forking a native reference-based session can discard its inherited history instead of copying the full context.

High
Category
Excessive Agency
Confidence
97% confidence
Finding

read_lines reads only the selected rollout, while write_branch removes history_base without resolving and materializing the referenced ancestor history. For a native reference-based source, the resulting branch can silently lose inherited user constraints, decisions, and tool evidence; this undermines the skill's central context-preservation guarantee and can cause a resumed agent to act without relevant safety constraints.

Content

No source excerpt is available for this finding.

Tainted flow: 'BRANCH_INDEX' from os.environ.get (line 35, credential/environment) → open (file write)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · fork_core/adapter_claude_code.py (reported line 200)May include surrounding context.

python
raise RuntimeError(f"分支索引路径异常,已拒写:{_target}(预期 {_expect})")
        notify(f"更新旁路分支索引 / updating sidecar branch index: {BRANCH_INDEX}", quietable=False)
        os.makedirs(CLAUDE_DIR, exist_ok=True)
        with open(BRANCH_INDEX, "w", encoding="utf-8") as f:
            f.write(dumps_safe(data, indent=2))

    def register_branch(self, src: SessionMeta, new_id: str, dst_path: str, name: str, parent_id: str = None, at_seq: int = None, prefix_fp: str | None = None) -> None:

Session-ID rewriting also alters historical message bodies and tool-result strings.

Medium
Category
Excessive Agency
Confidence
97% confidence
Finding

The recursive replacement modifies every string containing the source session ID except values under two excluded keys, including historical message bodies, tool outputs, and serialized tool arguments. This can silently change identifiers, paths, or evidence that the resumed agent treats as authentic history, making the behavior more dangerous in a skill promising faithful context duplication.

Content

No source excerpt is available for this finding.

Tainted flow: 'self.LINEAGE_PATH' from os.environ.get (line 141, credential/environment) → open (file write)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · fork_core/adapter_codex.py (reported line 619)May include surrounding context.

python
def _write_index(self, data: dict) -> None:
        os.makedirs(os.path.dirname(self.LINEAGE_PATH), exist_ok=True)
        with open(self.LINEAGE_PATH, "w", encoding="utf-8") as f:
            f.write(dumps_safe(data, indent=2))

    def list_branches(self, cwd: str | None = None) -> list[SessionMeta]:

Tainted flow: 'tmp' from os.environ.get (line 599, credential/environment) → open (file write)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · fork_core/adapter_hermes.py (reported line 600)May include surrounding context.

python
def _write_index(self, data: dict) -> None:
        os.makedirs(os.path.dirname(self.LINEAGE_PATH) or ".", exist_ok=True)
        tmp = self.LINEAGE_PATH + ".tmp"
        with open(tmp, "w", encoding="utf-8") as f:
            f.write(dumps_safe(data, indent=2))
        os.replace(tmp, self.LINEAGE_PATH)

Tainted flow: 'self.SESSIONS_INDEX' from os.environ.get (line 99, credential/environment) → shutil.copy2 (file write)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · fork_core/adapter_openclaw.py (reported line 140)May include surrounding context.

python
if os.path.exists(self.SESSIONS_INDEX):
            try:
                os.makedirs(self.backups_dir(), exist_ok=True)
                shutil.copy2(
                    self.SESSIONS_INDEX,
                    os.path.join(
                        self.backups_dir(),

Tainted flow: 'tmp' from os.environ.get (line 149, credential/environment) → open (file write)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · fork_core/adapter_openclaw.py (reported line 150)May include surrounding context.

python
except Exception:
                pass
        tmp = f"{self.SESSIONS_INDEX}.{os.getpid()}.{uuid.uuid4().hex[:6]}.tmp"
        with open(tmp, "w", encoding="utf-8") as f:
            f.write(dumps_safe(data, indent=2))
        os.replace(tmp, self.SESSIONS_INDEX)

Tainted flow: 'self.LINEAGE_PATH' from os.environ.get (line 89, credential/environment) → open (file write)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · fork_core/adapter_pi.py (reported line 350)May include surrounding context.

python
def _write_index(self, data: dict) -> None:
        os.makedirs(self.AGENT_DIR, exist_ok=True)
        with open(self.LINEAGE_PATH, "w", encoding="utf-8") as f:
            f.write(dumps_safe(data, indent=2))

    def register_branch(self, src, new_id, dst_path, name, parent_id=None, at_seq=None,

The adapter documents read-and-insert-only access to the official sessions table, but rollback deletes rows.

Medium
Category
Excessive Agency
Confidence
97% confidence
Finding

The module documentation at L006 explicitly limits official sessions-table operations to reads and new-row insertion. However, unregister_branch executes DELETE FROM sessions at L344 using the supplied ID, without verifying that the row was created by this adapter; this contradicts the documented non-deletion boundary.

Content

No source excerpt is available for this finding.

The --fix operation can truncate an original session rather than restricting changes to forked branches.

Medium
Category
Excessive Agency
Confidence
98% confidence
Finding

run_fix accepts any resolvable WorkBuddy session ID and overwrites its transcript with a shortened prefix without checking that it is a fork. A mistaken or adversarially selected original-session ID can therefore remove messages from the original conversation, violating the skill's promise to leave the original untouched; the backup reduces recovery cost but does not prevent the destructive modification.

Content

No source excerpt is available for this finding.

Verification claims malformed entries in the copied range block forking, but creation only warns and omits them.

Medium
Category
Excessive Agency
Confidence
98% confidence
Finding

Creation warns about malformed source entries but continues using the remaining parsed entries; verification then checks the rewritten output against that already-filtered sequence, so it cannot detect omitted source content. This contradicts the diagnostic claim that malformed entries within the copied range prevent forking and can produce a successfully verified branch missing instructions, conclusions, or tool results. Because this skill transfers agent session context, accidental corruption—or deliberate corruption by someone able to modify the transcript—can change the resumed agent's understanding without causing verification to fail.

Content

No source excerpt is available for this finding.

The mandatory reporting template requires Chinese output without offering a user-language choice.

Low
Category
Output Handling
Confidence
90% confidence
Finding

The skill requires reports to use a fixed template whose headings and explanatory text are in Chinese. Describing the skill as written for Chinese users and linking an English overview does not provide an opt-in or allow the agent to adapt this required output to the user's language.

Content

No source excerpt is available for this finding.

User-facing notifications are required to use Chinese and English without user opt-in.

Low
Category
Output Handling
Confidence
95% confidence
Finding

The notify docstring mandates that notification strings present Chinese and English together. This imposes specific output languages without offering a user choice or establishing a region-specific requirement; the general safety rationale does not explain why these two languages must be used.

Content

No source excerpt is available for this finding.

The headerless fallback rewrites entry IDs despite explicitly promising to preserve them.

Low
Category
Excessive Agency
Confidence
99% confidence
Finding

The docstring at L261 and comment at L299 explicitly state that entry IDs remain protected when the session header is missing. However, the fallback invokes walk(), which recursively replaces matching strings in every field except content and parentSession (L268–L276), including id and parentId; entry identifiers containing old_id are therefore modified contrary to the documented identity-preservation guarantee.

Content

No source excerpt is available for this finding.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · fork_core/adapters.py (reported line 117)May include surrounding context.

python
import importlib

        module = importlib.import_module(f".{module_name}", package=__package__)
        return register(getattr(module, class_name)())
    raise SystemExit(f"Unknown adapter: {name!r} (available: {', '.join(available())})")