chrissnell/graywolf/writing-graywolf-action-handlers
Claude Code and agent skill by chrissnell/graywolf
- community
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-handlersSKILL.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:
- POSIX shell (
bashfor Linux + macOS) — see `reference-posix.md` - PowerShell (Windows 10/11, PS 5.1 or
pwsh7) — see `reference-powershell.md` - Webhook (HTTP receiver, examples in Python Flask/aiohttp) — see `reference-webhook.md`
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:
| Integration | Naive scoping | What goes wrong | |
|---|---|---|---|
| AWS SNS with default IAM user / admin access key | Send any SMS | Attacker can pivot to S3, SES, Lambda, IAM, billing — full account takeover via the same key. | |
| Twilio with main account auth token | Send one SMS | Attacker can send unlimited SMS / MMS / voice, run up the bill, originate calls from your number. | |
| Home Assistant with a long-lived access token | Toggle one entity | Attacker can call any service: unlock doors, disable alarms, read every sensor. | |
| Generic SaaS API key | One endpoint | Attacker can usually reach every endpoint that key authenticates. | |
| SSH / pkexec / sudo from the handler | Run one command | Attacker reaches the underlying user's full shell privilege. |
The countermeasures are always the same:
- 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.
- 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.
- Cap the rate. Set
RateLimitSecon the Action so a runaway
sender cannot drain the credential's quota.
- 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?
| Class | Examples | Implications | |
|---|---|---|---|
| Read-only | weather lookup, ISS pass, uptime | OTP optional, allowlist optional, no extra hardening | |
| Notification | SMS, push, log entry | OTP recommended, allowlist required if cost-per-fire (Twilio etc.) | |
| State change, reversible | turn light on/off, post note | OTP required, allowlist recommended | |
| State change, irreversible / physical | open garage, unlock door, ignition, money movement | OTP 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:
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?
| Question | If yes | If no | |
|---|---|---|---|
| Are there 2+ structurally different inputs (entity + state, number + body)? | kv | go on | |
| Will the natural sender phrasing be one chunk of text (an SMS, a notification body)? | freeform | kv | |
| Will the value be passed straight through to a downstream API as one string? | freeform | kv |
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/noregex— 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
- SKILL.md in the repositoryCheckingexamples/skills/writing-graywolf-action-handlers/SKILL.md
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
[](https://verified-skill.com/skills/chrissnell/graywolf/writing-graywolf-action-handlers)