chrissnell/graywolf/writing-graywolf-action-handlers

Claude Code and agent skill by chrissnell/graywolf

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

Use when authoring a Graywolf Actions handler — POSIX/bash, PowerShell, or webhook receiver — that fires on inbound APRS messages of the form `@@<otp>#<action>`. Triggers when an operator says "write me an Action script", "scaffold an Action handler", "I need an SMS/garage/notify Action", or asks how to wire a script to a remote sender safely. Covers OTP/allowlist sizing, kv-vs-freeform mode, argument validation, and the safety invariants that keep an over-the-air payload from compromising the host.

Install

npx vskill@latest install chrissnell/graywolf/writing-graywolf-action-handlers

SKILL.md

Writing Graywolf Action handlers

Overview

A Graywolf Action handler runs locally — or on a webhook receiver you control — when a remote APRS sender transmits @@<otp>#<name> .... The payload arrives over the air and must be treated as untrusted input from a public radio frequency. A handler that mishandles it can shell-inject the host, smuggle SMS to arbitrary numbers, unlock the wrong door, or DOS a downstream API.

This skill walks an operator through the interview that shapes a correct handler and then emits an idiomatic, lint-clean script that follows every invariant the Graywolf project documents. Three flavors:

OTP and sender-allowlist sizing applies to all three: see `reference-otp-allowlist.md`.

Threat model — always Internet-hostile

Every Action handler is exposed to arbitrary attacker-controlled input from the public Internet and the APRS-IS / RF networks. This is true regardless of what the operator believes the exposure is:

  • APRS-IS is a public, federated, unauthenticated message bus. Any

station with an Internet connection can transmit to any addressee.

  • RF traffic is monitored at scale by aprs.fi, aprs2.net mirrors, SDR

hobbyists, and anyone within range of a digipeater. Sender callsigns can be forged on RF; SSID stripping further widens the impersonation surface.

  • The OTP and sender allowlist are probabilistic defenses — strong

defenses, but the script's safety must not depend on them holding. Write the script as if every byte of input is attacker-chosen.

The skill must proactively explain this to the operator if they say things like "this is just for me" or "only my friends will use it". Their model of the exposure is wrong; correct it before generating code.

Trust arguments are out of scope

"Only my friends," "only licensed hams," "I'm the only one with the OTP," "my callsign is on the FCC database so I'm verified" — none of these are authentication boundaries. The FCC license database is public, callsigns are forged routinely on APRS-IS, OTP is a 30-second / six-digit window with a knowable secret, and a sender allowlist is a callsign string match (no per-user identity). When the operator's argument for relaxing a defense is the identity of the senders, the skill must refuse and re-explain the threat model. When the argument is the cost of the defense (latency, complexity, operator effort), it can be weighed. Identity-based arguments never weigh.

The threat is not just side effects — read-only Actions can leak

An Action that "only reads" can still exfiltrate state. journalctl -u <pattern> discloses logs from services the sender should not see; cat /etc/<file> discloses arbitrary file contents the service account can read; kubectl logs -l <selector> discloses pod logs; Get-EventLog -LogName Security discloses authentication history. Apply the same blast-radius reasoning to read-only Actions: what can the underlying user account read? That is the worst-case disclosure surface, regardless of what the operator believes the Action exposes.

Blast radius — what does this handler actually let me do?

Most handlers integrate with an external service (Twilio, AWS, Home Assistant, a smart-home cloud, a VoIP provider, a downstream webhook). The handler holds the credentials for that service. A handler bug or a regex gap does not just expose the one Action — it exposes everything the credential can do.

Walk the operator through this explicitly during the interview. The question is not "what does this Action do?" — it is "if a remote attacker successfully drove this handler with arbitrary input, what could they reach?" Concrete examples:

IntegrationNaive scopingWhat goes wrong
AWS SNS with default IAM user / admin access keySend any SMSAttacker can pivot to S3, SES, Lambda, IAM, billing — full account takeover via the same key.
Twilio with main account auth tokenSend one SMSAttacker can send unlimited SMS / MMS / voice, run up the bill, originate calls from your number.
Home Assistant with a long-lived access tokenToggle one entityAttacker can call any service: unlock doors, disable alarms, read every sensor.
Generic SaaS API keyOne endpointAttacker can usually reach every endpoint that key authenticates.
SSH / pkexec / sudo from the handlerRun one commandAttacker reaches the underlying user's full shell privilege.

The countermeasures are always the same:

  1. Scope the credential as narrowly as the service allows.
  • AWS: a dedicated IAM user or role with a single sns:Publish

statement, scoped via Resource: to one topic ARN, plus a Condition: on the destination phone number prefix where possible. Never the AWS root key, never an admin policy, never Resource: "*".

  • Twilio: a sub-account auth token, not the master account.
  • Home Assistant: a long-lived access token tied to a *least-privilege

user* with limited entity access, not the operator's admin user.

  • SSH: a dedicated key with a command="..." restriction in

authorized_keys, or forced-command via the agent.

  1. Validate every byte of user input that touches the request.

E.164 number? Anchor the regex. State on|off? Anchor it. Entity name? Anchor it.

  1. Cap the rate. Set RateLimitSec on the Action so a runaway

sender cannot drain the credential's quota.

  1. Log the credential scope in the script's header comment so a

future reader can audit it without reading the IAM console.

The skill must explain the blast-radius concept in language the operator can act on, then ask the operator: "Show me — or describe — the credential this script will use. What is its scope?" If the operator does not know the answer, generate the script anyway but include a prominent # AUDIT THIS: block at the top listing the scoping work that must happen before the Action goes live.

When to use

  • Operator asks for a handler script for a specific Action (SMS, home

automation, notification, lookup, status, etc.).

  • Operator has a draft script and wants it reviewed against the

handler-safety contract.

  • A new Action will perform anything irreversible, anything that

touches money, anything that triggers a physical actuator, or anything that talks to an external API.

  • Operator asks "kv or freeform?", "do I need OTP?", "is this safe?".

Skip this skill for non-Action work (general bash, generic PowerShell, unrelated webhook receivers). The patterns here are specific to the Graywolf Actions execution contract.

Interview — ask these questions in order

Don't generate code until every question is answered. Ask the operator one block at a time; combine answers into the script.

1. What does the Action do?

One sentence. Examples:

  • Sends an SMS via Twilio.
  • Toggles a Home Assistant light.
  • Returns current solar weather.
  • Posts a status note into a webhook receiver that logs to SQLite.

Capture the verb and the side effect. The verb decides the Action's name. The side effect decides everything below.

2. What is the side-effect class?
ClassExamplesImplications
Read-onlyweather lookup, ISS pass, uptimeOTP optional, allowlist optional, no extra hardening
NotificationSMS, push, log entryOTP recommended, allowlist required if cost-per-fire (Twilio etc.)
State change, reversibleturn light on/off, post noteOTP required, allowlist recommended
State change, irreversible / physicalopen garage, unlock door, ignition, money movementOTP required, allowlist required, narrow regex on every arg

If the operator is uncertain, default up one tier. Ask: "If a stranger sent this Action with valid OTP, what is the worst they could do?"

3. Who can trigger it?
  • Just me. → Sender allowlist with the operator's callsign(s) only.
  • A small group (family, club). → Allowlist with each callsign.

Mention SSID handling: graywolf strips SSID for the comparison (base-call match), so KE0XYZ-9 and KE0XYZ both match KE0XYZ.

  • Anyone on the band. → No allowlist. Only acceptable for read-only

Actions or Actions whose side effect is harmless if abused.

The classifier checks the allowlist before the OTP probe, so a denied sender cannot enumerate which OTP digits validate. Operators do not need to design around this — just recommend the allowlist when the side-effect class warrants it.

4. OTP credential — yes or no, and is the script defending in depth?
  • Yes for any side-effect class except read-only.
  • Yes if the Action consumes a paid API.
  • No is acceptable for pure-information Actions whose worst-case

abuse is a small amount of RF chatter.

If yes, the operator must mint an OTP credential in /#/actions → Credentials before saving the Action. The skill should remind them: "Create the OTP credential first; the Action's OTP credential dropdown will be empty until you do."

The station is single-user; do not invent issuer / account fields. The issuer is always Graywolf and the account is always the station callsign (see feedback_single_user_station).

Defense in depth: the script must also check `GW_OTP_VERIFIED`. The Action editor's OTPRequired toggle is the first defense; an operator can flip it off in the UI without re-deploying the script. Every generated handler that performs a side effect must therefore re-assert OTP at the script level:

bash
# bash
[[ "${GW_OTP_VERIFIED:-false}" == "true" ]] || { echo "otp not verified" >&2; exit 77; }
powershell
# PowerShell
if ($env:GW_OTP_VERIFIED -ne 'true') {
[Console]::Error.WriteLine("otp not verified")
exit 77
}
python
# Webhook receiver (Flask)
if request.form.get("otp_verified", "false") != "true":
abort(401)

Skip this only for genuinely read-only Actions where unauthenticated RF chatter is acceptable. Do not skip for anything that costs money, moves a physical thing, or persists data downstream.

5. Wire format — kv or freeform?
QuestionIf yesIf no
Are there 2+ structurally different inputs (entity + state, number + body)?kvgo on
Will the natural sender phrasing be one chunk of text (an SMS, a notification body)?freeformkv
Will the value be passed straight through to a downstream API as one string?freeformkv

Default to kv when in doubt. Freeform is friendlier on-air for text-y payloads but pushes the splitting + revalidation onto the handler script, which the operator must then write correctly.

6. Arguments — names, regexes, required?

For kv mode, list every argument with:

  • key — short, lowercased, [a-z][a-z0-9_]*
  • required — yes/no
  • regex — anchor with ^ and $. Make it as narrow as the input

shape allows. Examples in `reference-otp-allowlist.md`.

For freeform mode, the schema has a single synthetic row keyed arg. The script does the splitting in one regex match (see the platform-specific reference). The schema's regex still applies as a first-pass filter; do not skip it.

7. Platform + invocation surface?

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
157
Repository forks
16
7-day trend score
25

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/chrissnell/graywolf/writing-graywolf-action-handlers/badge)](https://verified-skill.com/skills/chrissnell/graywolf/writing-graywolf-action-handlers)

Something wrong with this skill or its page? Tell us.