跳到主要内容

Hooks & webhooks

A hook is a registered command body that fires over HTTP when the local listener receives a POST /hook/<id> with a matching X-Hook-Token. Use hooks to wire TradingView alerts, external scripts, or your own automation to the same command grammar you type at the REPL. Standalone-only — hooks live inside the listener subsystem, see Listener for the underlying transport.


REPL commands

CommandSyntaxNotes
hook / hook list / hooksbareList registered hooks. Secrets shown as <hidden> — use hook show to reveal.
hook createhook create <name> "<command>" [--account <name>]Mint a new hook. Prints the fire URL + X-Hook-Token. Quotes: " or ', no escapes.
hook show <id|prefix>hook show <id|prefix>Reveal the stored secret and re-print fire instructions.
hook rotate <id|prefix>hook rotate <id|prefix>Mint a new secret in place. Old secret stops working immediately.
hook revoke <id|prefix|--all>hook revoke <id|prefix|--all>Delete one or all hooks. Prefix must be ≥ 8 chars; ambiguous prefixes are refused.

Quoting rule for the command body: use "…" or '…'. There is no escape syntax inside the quotes — pick whichever quote your body doesn't contain.

hook create alert-long "buy $250 SOLUSDT at -0.5%"
hook create flatten "nuke positions" --account mybybit
hook revoke 4f3a1c8e
hook rotate 4f3a1c8e

Token model

FieldShapePurpose
id16 random bytes → 32 hex charsGoes in the URL path: /hook/<id>. Non-secret.
secret16 random bytes → 32 hex charsSent as the X-Hook-Token HTTP header. Stored in hooks.json.

Verification uses crypto.timingSafeEqual, but the implementation length-checks first. The unequal-length call to timingSafeEqual would throw RangeError and bubble up as HTTP 500; the length guard keeps the wrong-token path returning a clean 401. Don't remove the guard if you're touching verifyHookToken().


Per-account scope

hook create … --account <name> pins the hook to a specific account. When the hook fires it sets accountOverride on the run's AsyncLocalStorage, so the command executes against that account regardless of what the REPL is currently focused on.

Hooks created without --account execute against whatever account the REPL has focused at fire time. That's fine for hooks you want to follow your manual context, but explicit pinning is the safer default for unattended fires.

The web app can create hooks via mutate:hook over the WS (see Listener → wire frames) — same account field, same semantics.


HTTP fire shape

POST http://127.0.0.1:53219/hook/<id>
X-Hook-Token: <32-hex secret>

The request body is ignored — the command is whatever was registered at hook create time. Useful because most webhook senders (TradingView, alerting tools) only let you customize URL and headers, not control which command runs.

StatusBodyMeaning
200{ ok: true, runId, command, account? }Accepted. Command runs asynchronously; HTTP returns immediately.
401{ ok: false, error: 'invalid token' }Token missing, wrong length, or wrong value. Length-guarded before constant-time compare.
404{ ok: false, error: 'unknown hook' }No hook with that id.
423{ ok: false, error: 'remoteExec disabled' }listener.remoteExec is false. See Listener → kill switches.
503{ ok: false, error: 'listener disabled' }listener.enabled is false or the server isn't bound.

Fire-and-forget: the 200 returns the moment the hook is accepted. The actual run streams cmd:output frames on the hook-output WS topic, then a final cmd:done. Web clients subscribed to that topic see the run as if it were a cmd:exec.

$ curl -X POST http://127.0.0.1:53219/hook/4f3a1c8e... \
-H "X-Hook-Token: 8b9d2e4a..."
{"ok":true,"runId":"r_01HK…","command":"buy $250 SOLUSDT at -0.5%"}

TradingView (and other senders)

127.0.0.1 is unreachable from the public internet, so TradingView (or any external sender) needs a tunnel that terminates on your loopback address. Any tunnel works — Cloudflare Tunnel, ngrok, an SSH reverse forward — point it at 127.0.0.1:53219 and use the public hostname in the TradingView webhook URL. Keep the X-Hook-Token header on the public side; without the tunnel the listener would refuse the inbound origin anyway.

End-to-end walkthrough: Webhook from TradingView.


Local mirror

Every cmd:output frame a hook produces is also printed to the local REPL with a [hook:<name>] prefix in the matching chalk color. You see exactly the same output a paired web client would see, without needing one attached. Useful for debugging the command body without flipping back to the web tab.


Lifecycle

  • Hooks persist to ~/.tealstreet/hooks.json on every mutation. The file contains shared secrets — back it up the same way you'd back up any credential file.
  • Hooks survive REPL restart. Revoking a hook is the only way to invalidate it; restarting the CLI does not rotate secrets.
  • Hook fires are gated by listener.remoteExec. Toggling that flag to false is the fastest way to disable every hook at once without deleting them — the kill switch returns HTTP 423 and the hook records stay intact.

Related: Listener for the underlying server, Trusted clients for WS-level access control, Pairing for the end-to-end web setup.