Writing safe Action handlers

An Action handler is a handler — a shell script, a PowerShell script, a webhook receiver — that you control. graywolf gives it a payload from a remote APRS sender. Your handler decides what to do with that payload. Anything an attacker can persuade your handler to do, they can do over the air.

This page is the overview: the two argument modes, the contract graywolf passes to your handler, and the final safety checklist that applies to every handler.

Three sub-pages drill into worked examples for each handler kind, with line-by-line explanations and anti-patterns to avoid:

Every example is also shipped in examples/actions/ in the source tree.

Two argument modes

Each Action has a mode that controls how the wire-format arguments reach your handler:

ModeWire formatUse when
kv (default) @@<otp>#name k1=v1 k2=v2 You have multiple, named, individually-validated arguments.
freeform @@<otp>#name <raw text> Everything after the verb is one payload (e.g. an SMS).

Pick freeform when you want a sender to type something natural like @@482910#sms +15555551212 hello there instead of @@482910#sms to=+15555551212 msg="hello there". The cost: the operator (you) takes on more responsibility for parsing and revalidating the payload. The sub-pages are about discharging that responsibility correctly.

kv vs freeform — when to pick which

What graywolf passes to your handler

Command Actions (shell or PowerShell)

The runner exec's your binary directly — no shell, no PATH lookup funkiness, no system(). argv layout depends on mode:

kvfreeform
$1 / $args[0]action nameaction name
$2 / $args[1]sender callsignsender callsign
$3 / $args[2]true | false (OTP verified)same
$4..$N / $args[3..N]k=v tokens, schema orderthe entire raw payload, one token

Environment variables are also set:

Webhook Actions

graywolf POSTs URL-encoded form data by default (or your custom template body if you set one). Default form fields:

Custom body templates support these tokens:

Reply policy and multi-line output

For successful invocations (status ok) graywolf may emit up to the Action's Max reply lines setting (default 1, ceiling 5) as separate APRS messages. Each non-empty stdout line becomes one outbound DM with its own message id, ack, and retries. Lines are capped at 67 characters; longer lines are truncated with . Blank lines are dropped.

Failure statuses (bad_arg, timeout, denied, etc.) always collapse to a single reply regardless of Max reply lines — fanning out a failure across multiple frames is never useful.

Each extra line is one extra RF frame plus its own ack and retry window. Keep Max reply lines at 1 unless your handler genuinely needs to deliver multiple short messages.

Final safety checklist

Before deploying any Action handler:

  1. Run shellcheck (shell), Invoke-ScriptAnalyzer (PowerShell), or ruff (Python). Fix everything it flags. Don't disable rules.
  2. Quote every variable expansion. Search for $GW_ not preceded by ".
  3. Confirm there is no eval, no sh -c with user data, no Invoke-Expression.
  4. Confirm any external command takes user data via ---terminated argv (or array -ArgumentList in PowerShell), not concatenated strings.
  5. Confirm any database write uses parameterized placeholders.
  6. Confirm any HTML render uses framework auto-escape.
  7. For webhooks: confirm the receiver authenticates (HMAC / shared secret), caps body size, and is reachable only via TLS.
  8. Test the handler with payloads designed to break it: leading -, embedded ' and ", & and =, max-length values. Use the Test dialog in the operator UI for this — it runs the handler bypassing OTP and the sender allowlist.