Back to skill

Security audit

BTC K14 Paper Trader

Security checks for vulnerabilities and agentic risk

Overview

The skill’s bounded trading simulation matches its description, with no real-money execution path identified.

Review the automatic schedule before enabling it. Supply the Simmer API key through secret configuration, protect and reuse the state directory, and run only one instance per key. Create STOP in that directory to halt activity. Results are simulations, not evidence of real trading profits; the bundled thresholds expire after October 2026.

Vulnerability Patterns
  • 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
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (26)

Description-Behavior Mismatch

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The code's primary workflow is reading an existing event file and dispatching its recorded method calls to PaperEngine. It explicitly labels the output as synthetic offline replay rather than a strategy backtest. Its journal and intent exports could support ledger-related functionality, but they do not establish the declared complete strategy execution or separate Simmer mirror. PaperEngine internals are not supplied, so their capabilities cannot be verified; the visible entry point nevertheless has a materially different primary purpose.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

The primary behavior shown is exit monitoring, not strategy execution. Startup requires order_submission to equal 'NONE', and the observation loop records actual_orders_sent as zero. Although snapshot evaluation supports the declared dynamic-exit functionality, this chunk does not implement paper position execution, trading-ledger management, or Simmer mirroring. Its local polling, diagnostic persistence, and status API are supporting implementation details, not independently suspicious capabilities. Other files could implement the broader strategy, but the supplied chunk alone materially differs from the declared complete-run purpose.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding

The supplied chunk supports the declared dynamic-exit functionality, but its implemented purpose is read-only, hypothetical route comparison. Both result paths set execution_allowed to false, and route calculations assume an unconfirmed first leg rather than establish a position or execute an exit. No persistence or Simmer integration appears in this chunk. Thus, the description materially overstates the behavior shown; this finding is limited to the supplied code, not unseen parts of the project.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding

Dynamic exits and receipt accounting align with part of the description. However, LIVE_CONFIRMED is an explicitly supported operating mode, and _command marks its commands as non-paper; this is a concrete capability beyond paper-only operation. The code does not directly access a broker, network, credentials, or disk, so it should not be described as executing live trades or persisting a ledger itself. Entry selection, account coordination, durable persistence, and mirror execution must be supplied externally; their existence elsewhere cannot be established from this chunk.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding

The material discrepancy is in _cmd(): paper_only is set to false when provenance is LIVE_CONFIRMED. Additional live-specific receipt and terminal-proof handling confirms that the component is designed to support a live execution domain, not solely paper operation. This does not establish that the code sends real orders: transmission and authorization are explicitly delegated to an outer coordinator. Persistent ledger writes and a separate Simmer mirror are not implemented in this chunk; checkpoint/restore only supplies state for external persistence. Those omissions alone are not treated as mismatches because this may be a supporting component of a larger skill.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding

The code substantially overlaps the declared strategy through K14 signals, market validation, and risk gating. However, its explicit intent-only behavior materially narrows the advertised execution capability: orders are reported as UNKNOWN_PUBLIC_QUEUE, realized P&L remains null, and uncertain reserves remain held rather than being resolved through simulated fills. The conditional hedge calculation is a quote-based what-if, not an executed exit. Journal emission and in-memory session fields could support external persistence, but this chunk does not establish a persistent ledger or Simmer mirror. There is no evidence of unrelated resource access or undeclared privileged activity.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

High
Category
MCP Tool Poisoning
Confidence
91% confidence
Finding

The supplied entry point executes predetermined scenarios covering fills, cancellations, hedging, settlement, and feed gaps. It validates replay results and exports demonstration artifacts. Although these tests and journal files could support the declared strategy, this chunk's actual executable purpose is synthetic validation, not operating the described complete strategy and execution mirror. The imported replay implementation is not supplied, so its internal capabilities cannot be verified.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

High
Category
MCP Tool Poisoning
Confidence
90% confidence
Finding

The worker selects events exclusively from predefined research scenarios and reinitializes its engine for each run. Its visible behavior is offline simulation and telemetry, which materially narrows the declared primary purpose. It forwards accounting events to another process, so persistence could exist elsewhere, and strategy logic could reside in PaperEngine; however, neither those components nor a Simmer integration are demonstrated by this chunk. There is no evidence of unrelated resource access or malicious behavior.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

High
Category
MCP Tool Poisoning
Confidence
88% confidence
Finding

Persistent storage and process supervision are consistent supporting features. However, the visible behavior centers on synthetic scenario execution: start commands must reference cases returned by describe_cases(), and account fields mark actual strategy profit and fill probability as unknown. This materially differs from the declared complete strategy and Simmer-mirror workflow. Imported worker and live-process implementations were not supplied, so this assessment is limited to the visible code and does not establish that those components lack additional capabilities.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

High
Category
MCP Tool Poisoning
Confidence
87% confidence
Finding

The code supports parts of the description through shadow decisions, session persistence, and audit archives. However, its concrete behavior centers on public market-data collection and frozen shadow evaluation. It contains no visible Simmer client, execution-mirror calls, or separate SIM execution state. Strategy details are delegated to LiveShadow, whose implementation is not supplied, so dynamic exits and complete K14 execution cannot be verified here. The public network requests and local persistence are supporting implementation details, not independent mismatches; no credential use or live-order submission is visible.

Content

No source excerpt is available for this finding.

Skill declares no tool scope ('permissions' or 'allowed-tools') but code capabilities were detected: env, file_read, file_write, network.

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding

Without declared permissions the skill's intent is opaque and cannot be validated.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · vendor/backend/src/pm_predict/market_data/public_models.py (reported line 60)May include surrounding context.

python
cfg, fees = m.get('cryptoMarketConfig') or {}, m.get('feeSchedule') or {}
    rules = (cfg.get('asset') == 'btc' and cfg.get('duration') == '15m' and
             cfg.get('twapEnabled') is True and cfg.get('twapLookbackSeconds') == 60 and
             m.get('resolutionSource') == 'https://data.chain.link/streams/btc-usd-twap-60s-streams' and
             m.get('eventStartTime') == iso(start) and m.get('endDate') == iso(start+900))
    fees_ok = (m.get('feesEnabled') is True and fees.get('rate') == .07 and
               fees.get('exponent') == 1 and fees.get('takerOnly') is True)

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · vendor/backend/src/pm_predict/runtime/live.py (reported line 97)May include surrounding context.

python
samples=[]
                for _ in range(3):
                    before=wall_ms();m0=time.perf_counter_ns()
                    data,after=await self.get('https://api.binance.com/api/v3/time',lane='clock')
                    rtt=(time.perf_counter_ns()-m0)/1e6
                    if abs((after-before)-rtt)>50:raise ValueError('wall-clock step during sample')
                    samples.append({'at_ms':after,'offset_ms':data['serverTime']-(before+after)/2,

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · vendor/backend/src/pm_predict/runtime/live.py (reported line 109)May include surrounding context.

python
await asyncio.sleep(60)

    async def candle_bootstrap(self,start):
        rows,_=await self.get('https://api.binance.com/api/v3/klines',
            {'symbol':'BTCUSDT','interval':'15m','endTime':start*1000-1,'limit':14},lane='candles')
        bars=[{'start':x[0]//1000,'high':float(x[2]),'low':float(x[3]),'close':float(x[4])} for x in rows]
        if any(x[6]>=start*1000 for x in rows):raise ValueError('unclosed bootstrap candle')

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · vendor/backend/src/pm_predict/runtime/live.py (reported line 129)May include surrounding context.

python
if not self.reference_clock.anchor:
                    await asyncio.sleep(.1);continue
                boundary=self.utc_ms()//60000*60
                rows,_=await self.get('https://api.binance.com/api/v3/klines',
                    {'symbol':'BTCUSDT','interval':'1m','endTime':boundary*1000-1,'limit':61},lane='volatility')
                v=Volatility()
                for x in rows:

The paper-mode skill supports generating explicitly non-paper exit commands for live-confirmed positions.

Medium
Category
Excessive Agency
Confidence
99% confidence
Finding

The manifest limits execution to paper mode and a Simmer virtual SIM mirror, but L015 and L081–L084 explicitly accept LIVE_CONFIRMED positions. At L128–L145, that provenance causes commands to carry paper_only=False, including PLACE and MERGE instructions; cancellation commands use the same construction at L147–L152. This file does not itself submit orders, but it provides a live-command generation path beyond the declared simulation scope.

Content

No source excerpt is available for this finding.

The paper-mode strategy can generate commands explicitly marked as not paper-only.

Medium
Category
Excessive Agency
Confidence
93% confidence
Finding

The manifest limits the strategy to paper mode and a virtual SIM execution mirror, but L069 sets paper_only=False whenever provenance is LIVE_CONFIRMED. This applies to order-placement commands generated at L231, allowing live-capable order intents rather than enforcing the declared simulation boundary. This file does not send orders itself; actual execution remains dependent on the outer coordinator.

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 · account.py (reported line 27)May include surrounding context.

python
if abs(gross-computed)>D('1e-20'):raise ValueError('cash gross differs from share price evidence')
        self.cash=str(dec(self.cash)+(-gross-f if action=='BUY' else gross-f))
        field_name='buy_gross' if action=='BUY' else 'sell_gross'
        setattr(self,field_name,str(dec(getattr(self,field_name))+gross))
        self.exchange_fees=str(dec(self.exchange_fees)+f);self.applied[receipt_id]=value
        if dec(self.cash)<0:raise ValueError('confirmed cash became negative; investigate external funds/fees')

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 · vendor/backend/src/pm_predict/engine.py (reported line 18)May include surrounding context.

python
self.state = EngineState()

    def __getattr__(self, name):
        return getattr(self.state, name)

    def reserve(self, *args, **kwargs):
        return domain_queries.reserve(self.state, *args, **kwargs)

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 · vendor/backend/src/pm_predict/execution/simmer_contract.py (reported line 36)May include surrounding context.

python
def compatibility(capabilities=INSPECTED_SIMMER_PAPER):
    missing = [name for name in REQUIRED if not getattr(capabilities,name)]
    return {'ready':not missing,'missing':missing,
            'mode':'PAPER_ONLY','execution_enabled':False,
            'reason':'Native instant paper fills are not a Maker/cancel/FOK/merge simulation' if missing else 'Capability contract met; separate receipt/provenance and durable-outbox integration required'}

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 · vendor/backend/src/pm_predict/research/replay.py (reported line 31)May include surrounding context.

python
continue
        seen[key] = canonical
        before = perf_counter_ns()
        getattr(e, event["method"])(**event["args"])
        durations.append(perf_counter_ns() - before)
    result = e.summary()
    result.update({"input_events": len(seen), "duplicates_ignored": duplicate_count,

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 · vendor/backend/src/pm_predict/runtime/live.py (reported line 117)May include surrounding context.

python
for b in self.closed_trade_bars:
            if b.start in reference:
                result=reference[b.start]
                if any(abs(getattr(b,k)-result[k])>.0000001 for k in ('high','low','close')):
                    self.event('CANDLE_MISMATCH',{'trade':vars(b),'rest':result})
                    raise ValueError('contiguous trade candle differs from official REST; do not lock this signal')
        if self.closed_trade_bars:self.event('CANDLE_CHECK',{'checked':sum(b.start in reference for b in self.closed_trade_bars),'pass':True})

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 · vendor/backend/src/pm_predict/runtime/worker.py (reported line 97)May include surrounding context.

python
raise ValueError('conflicting event id')
                duplicate = True
            else:
                getattr(engine, event['method'])(**event['args'])
                seen[event_id] = event
                duplicate = False
            timings.append(time.perf_counter_ns()-stamp)

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 · vendor/backend/src/pm_predict/strategy/parallel_exit.py (reported line 489)May include surrounding context.

python
'initial_qty':str(self.initial_qty),'initial_cost':str(self.initial_cost),
                'initial_gross_cost':str(self.initial_gross_cost),
                'initial_entry_fee':None if self.initial_entry_fee is None else str(self.initial_entry_fee),
                'amounts':{k:str(getattr(self,k)) for k in ('own','own_cost','other','other_cost','net_cashflow','realized')},
                'orders':{k:asdict(v) for k,v in self.orders.items()},
                'receipts':{k:[str(v) if isinstance(v,D) else v for v in p] for k,p in self.receipts.items()},
                'fees':{k:{**v,'amount':None if v['amount'] is None else str(v['amount']),'estimate':str(v['estimate'])} for k,v in self.fees.items()},

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 · vendor/backend/src/pm_predict/strategy/parallel_exit.py (reported line 494)May include surrounding context.

python
'receipts':{k:[str(v) if isinstance(v,D) else v for v in p] for k,p in self.receipts.items()},
                'fees':{k:{**v,'amount':None if v['amount'] is None else str(v['amount']),'estimate':str(v['estimate'])} for k,v in self.fees.items()},
                'reconciliations':{k:[str(v) if isinstance(v,D) else v for v in p] for k,p in self.reconciliations.items()},
                'meta':{k:getattr(self,k) for k in ('command_seq','now_ms','pair_attempts','last_pair_ms','last_pair_book','last_sell_book','last_submit_book','last_replace_ms','halted','merge_failed','settled')}}

    @classmethod
    def restore(cls, state):