This repository is real-money-capable. Everything in this document
should be read with that fact in mind — the usual "worst case: some data
leaks" framing for a typical web app does not apply here; the worst case
for a bug or credential leak in this specific repo is direct financial
loss. See CLAUDE.md's top banner for the evidence establishing this.
- What's sensitive:
HL_SECRET_KEY(the agent wallet's private key — full signing authority over that wallet's trading, though not withdrawal, per the agent-wallet model) and, to a lesser degree,HL_ACCOUNT_ADDRESS(a public address, not secret by itself, but reveals which account is being traded). - Where it lives: Only in
.env(gitignored via.gitignore's.envline; no.envfile exists in this repo as audited — only the placeholder.env.example, confirmed to contain no real key). Loaded intoos.environbyconfig.py's customload_dotenv(), read once intoConfig.secret_key, and passed toeth_account.Account.from_key()only whendry_run=False. - In-memory only, plaintext, for the process lifetime. No OS
keychain integration, no encryption at rest, no secret-manager
integration. The entire security boundary is: (1)
.envnever gets committed to git, (2) normal OS file permissions on the local machine. This is a reasonable model for a single-user local script, but it does mean anyone with read access to the machine's filesystem (or to a backup/sync of it, e.g. Time Machine, iCloud Drive if~/Projectswere ever synced there, a misconfigured dotfile sync tool, etc.) has the key. - Never logged. No code path prints, logs, or otherwise surfaces
HL_SECRET_KEY's value — confirmed via repo-wide reading of everyprint()call inbot.py/hl_client.py. The one place a live order's response is printed ("LIVE order -> {result}"inmarket_order()) echoes whatever the SDK'sExchange.market_open()returns — this was not independently verified to never include sensitive data (out of scope for a static audit; the SDK is a third-party dependency), but order-response payloads from exchange APIs are not typically credential data. - Mitigation already in place, verified via
README.md+ code: the documented setup flow uses a Hyperliquid agent wallet rather than the user's main wallet key — an agent wallet can trade but cannot withdraw, so a leaked key's worst case is "attacker can place/close trades on your behalf" (still real financial risk via bad trades / intentional loss-inducing orders) rather than "attacker drains your entire balance." This bound only holds if the user actually follows the agent-wallet instructions — nothing in the code enforces thatHL_SECRET_KEYis specifically an agent key rather than a main wallet key; that's entirely on the user.
- The live-order code path is fully functional, not a stub — see
CLAUDE.md/API_REFERENCE.md. OnceDRY_RUN=false,USE_TESTNET=false, andALLOW_MAINNET=yesare all set, every subsequent loop iteration can place real market orders sized up toMAX_POSITION_USDand continue trading untilDAILY_MAX_LOSS_USDis lost (kill switch) or the process is stopped. - The default state is safe. With no
.envpresent (this repo's actual state), or with.envcopied unedited from.env.example, the bot cannot place any real order — it's dry-run + testnet by default, and mainnet additionally requires the explicitALLOW_MAINNET=yesflag on top ofUSE_TESTNET=false. This three-switch design is a genuinely good defense-in-depth pattern against accidental real-money exposure from a stale or partially-edited.env. run-crazy.shis a real-money risk surface if.envis misconfigured. It does not overrideDRY_RUN/USE_TESTNET— only strategy/risk parameter values (MAX_POSITION_USD=20000,LEVERAGE=20,DAILY_MAX_LOSS_USD=995). Its own comment says "FAKE MONEY ONLY," but nothing in the script enforces that if.envhappens to already be configured for live mainnet trading. Recommendation: have this script explicitly exportDRY_RUN=trueitself so it can never be anything but a simulation, regardless of.env's state. SeeTASKS.mdTASK-004 — this is a real trading-behavior change and needs explicit user confirmation before being made.- No per-trade or intraday drawdown limit beyond the session-total kill
switch. A single bad order, up to
MAX_POSITION_USDin size, could in principle lose a meaningful fraction ofDAILY_MAX_LOSS_USDin one move before the kill switch's next check (check_kill_switch()is only evaluated once per loop iteration, at the top of the loop — a large, fast adverse price move within onePOLL_SECONDSwindow is not protected against until the next iteration reads equity again). - No upper bound on
LEVERAGE.config.pyonly enforces `LEVERAGE= 1
; a.env(orrun-crazy.sh`-style override) can set arbitrarily high leverage.
- Enforced in-process, every loop iteration, before any order is
sent:
RiskManager.clamp_order_notional()(position cap) andRiskManager.allows_order()(minimum order size). Enforced every loop iteration, before the signal is even computed:RiskManager.check_kill_switch()(session loss cap). - This enforcement is convention-based, not structurally guaranteed.
Nothing prevents a future code change from calling
client.market_order()/client.close_position()directly, bypassingrisk.pyentirely. There is no access-control layer, no assertion, no test that would catch this today (seeARCHITECTURE.md's architectural risk #2,TASKS.mdTASK-003 for the recommended test coverage that would at least partially guard against a regression here). - The kill switch halts the process; it does not prevent it from being
restarted immediately with the same
.env(and thus the same effective daily loss cap re-baselined against whatever equity exists at the new start). There is no persisted "already lost the daily max today, don't restart" state — a user (or an automated supervisor, if one were ever added) restarting the bot immediately after a kill-switch trip would reset the loss-tracking baseline (RiskManager.__init__setsstart_equityfresh each process start) and could, in principle, lose the same dollar amount again.
This is the most important unresolved risk in the codebase. See
ARCHITECTURE.md's "Major architectural risks" #1 for the full
technical detail. Summary:
bot.py's main loop only catchesRiskHaltandKeyboardInterrupt.- If
client.market_order()orclient.close_position()raises an exception for any reason (network timeout, connection drop, malformed API response, an SDK-internal exception) after the order may have already been transmitted to (and potentially filled by) the exchange, the exception is uncaught and crashes the process with a raw traceback. - The bot has no way to know, at the moment of the crash, whether the order succeeded, partially filled, or never reached the exchange at all.
- There is no automatic retry, no automatic reconciliation-on-restart check, no alert. A human must notice the process died, manually check the actual account state on Hyperliquid, and decide what to do.
- Mitigating factor: because live-mode position/equity reads
(
position_size(),account_value()) always query the real exchange fresh (Info.user_state()), a restarted bot process will correctly pick up whatever the true post-crash position actually is — the bot does not maintain a stale local copy of position state that could drift from reality. The risk is specifically the gap between the crash and a human/restart noticing it, not permanent state corruption. - Recommendation (not implemented, flagged for
TASKS.md): wrap the order-placement calls inbot.py's loop with a try/except that at minimum logs the failure clearly and attempts to re-read/report the actual current position before exiting, rather than a bare traceback. This is a behavior change to code adjacent to order execution and needs explicit user confirmation before being implemented, perCLAUDE.md.
hyperliquid-python-sdkis unpinned inrequirements.txt. A version bump on reinstall could change the behavior or signature ofExchange.market_open()/market_close()/update_leverage()— the exact functions responsible for real order placement — without any explicit review. SeeTASKS.mdTASK-001.- No dependency vulnerability scanning (no
pip-audit, no Dependabot config, nosafetycheck) configured anywhere in the repo. eth_account(used for key handling) is a transitive dependency of the SDK, not directly pinned either — same concern as above, one level removed.
A repo-wide search for hardcoded credentials, API keys, and
plaintext-secret patterns was performed across all .py files. No
hardcoded secrets, credentials, or real key values were found anywhere in
the codebase. No .env file exists in this repo to leak from. The only
place any credential-shaped variable appears is as an empty placeholder
in .env.example (HL_SECRET_KEY= with no value, and a comment
explaining what belongs there) — this was read and confirmed to contain
no real value, and no value from it (there is none) was reproduced
anywhere in this documentation.
- High: Add explicit error handling around
market_order()/close_position()calls inbot.py's loop so a network failure mid-order produces a clear, actionable message and an attempt to verify true account state, rather than an uncaught crash. (Needs explicit user confirmation — touches order-execution-adjacent code.) - Medium-High: Decide and fix
run-crazy.sh's relationship to.env's safety switches (forceDRY_RUN=truein the script itself). (Needs explicit user confirmation — real trading-behavior change.) - Medium: Pin
hyperliquid-python-sdkinrequirements.txtto the currently-installed, presumably-tested version. - Medium: Add unit tests for
risk.pyspecifically (highest-value test target given real-money stakes). - Low-Medium: Consider a persistent, append-only trade/action log (not just stdout) for post-hoc auditability.
- Low: Consider an upper bound check on
LEVERAGEinconfig.py's validation, consistent with the existing>= 1check.
None of the above were implemented during this audit — this is a documentation-only pass per the task's explicit instructions.