Claude Code Buddy Skill
Connect to a device on the local network and push Claude Code events to it. After connecting, the Stop and Notification hooks fire automatically — this skill covers the on-demand actions (connect, notify, usage) and the plain-language config tuning.
The plugin only speaks the HTTP contract below. The device decides the actual LED / display / voice feedback; the plugin never draws anything itself.
Config
Path: ~/.config/claude-code-buddy.json (file permission 0600).
{
"devices": [
{ "label": "My Device", "host": "lamp-a1b2.local", "last_known_ip": "192.168.1.50", "password": "the-web-admin-password" }
],
"default_host": "lamp-a1b2.local",
"usage_threshold": 80,
"sounds_enabled": true,
"task_done_enabled": true,
"notify_enabled": true
}
hostis the device's mDNS hostname (likelamp-a1b2.local);last_known_ipis the cached address used to avoid re-resolving every time.passwordis the device's admin password — the same one used to log into the device's web UI. The daemon gates its LAN endpoints (notify / usage / approval) with it, sent as anAuthorization: Bearerheader; without it those calls return HTTP 401. Discovery (/health) stays open, so the device is still findable before a password is set. Stored only in this0600file.- Shared helpers live in
scripts/buddy_client.py(config load/save, discovery,send(), usage fetch). Prefer them over re-implementing.
1. Discover / Connect
Find the device on the LAN and save it to config.
When to use: "connect my device", "set up my device", or when a POST fails with a connection error/timeout (the cached IP is stale).
Run discovery
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/discover.py
How it works (automatic): cache check (cached IP → /health) → mDNS browse
for _autonomous._tcp → /24 subnet /health sweep. Returns the found
device(s) as JSON, e.g.:
[{ "host": "lamp-a1b2.local", "ip": "192.168.1.50" }]
On success: ask the user for the device's admin password (the same one
they use to log into the device's web UI) — the daemon requires it to accept
activity/approval pushes. Then write the device into devices[] (append new,
update existing by host) including the password field, set default_host if
it's the first device, and write the file with permission 0600.
If a later push returns HTTP 401, the stored password is missing or wrong
(or was changed in the web UI) — ask the user for the current admin password and
update the device entry.
On failure: exits non-zero with {"error": "not_found"} on stderr. Suggest:
device powered on? Same WiFi as the computer?
2. Notify
POST a notification to the device. Most common on-demand action.
When to use: "notify my device", "send to my device", "ping my device when done". Also reasonable proactively at the end of a long task.
2.1 Pick the target
Use default_host (and its last_known_ip) from config. If no config or no
devices → tell the user to connect a device first (section 1).
2.2 Build the payload
{
"title": "Build passed",
"subtitle": "3m 12s, 0 failures",
"level": "done",
"sound": true
}
title(string, required) — short headline, plain text.subtitle(string) — one line of detail.level—"done"|"attention"|"info". Useattentionwhen the user needs to act,donefor completions,infootherwise.sound(boolean) — whether the event carries a sound cue. The device picks the actual cue; do not send sound indices. Respectsounds_enabledfrom config.
2.3 Send (urllib, stdlib only)
import json, urllib.request
payload = json.dumps({
"title": "Build passed",
"subtitle": "3m 12s",
"level": "done",
"sound": True,
}).encode()
req = urllib.request.Request(
"http://<ip>:5002/claude-code/notify",
data=payload,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=3) as resp:
print(resp.status) # expect 200, body {"ok": true}
Replace <ip> with the device's last_known_ip (or resolve host).
3. Usage
Fetch the user's OAuth usage and POST it to /claude-code/usage.
When to use: "push my usage to my device", "/claude-code-buddy:usage".
3.1 Fetch usage
Read the OAuth token (~/.claude/.credentials.json, or the macOS keychain item
Claude Code-credentials; the token starts with sk-ant-oat), then:
import json, urllib.request
req = urllib.request.Request(
"https://api.anthropic.com/api/oauth/usage",
headers={
"Authorization": f"Bearer {token}",
"anthropic-beta": "oauth-2025-04-20",
},
)
with urllib.request.urlopen(req, timeout=15) as resp:
usage = json.loads(resp.read())
from buddy_client import time_left
five_hour = int(usage["five_hour"]["utilization"])
seven_day = int(usage["seven_day"]["utilization"])
reset_5h = time_left(usage["five_hour"]["resets_at"]) # -> "1h 56m"
reset_7d = time_left(usage["seven_day"]["resets_at"]) # -> "1d 11h"
scripts/buddy_client.py already wraps token lookup + fetch — use it.
3.2 POST to /claude-code/usage
{
"five_hour": 41,
"seven_day": 48,
"reset_5h": "1h 56m",
"reset_7d": "1d 11h",
"sound": true
}
reset_5h / reset_7d are display-ready strings (humanize the resets_at
ISO timestamp with buddy_client.time_left()), not raw ISO — the device shows
them as-is.
POST to http://<ip>:5002/claude-code/usage exactly like section 2.3 (expect 200,
{"ok": true}). The usage API rate-limits hard — do not poll faster than once
per minute.
4. Task Done + "Claude needs you" (automatic)
These run via Claude Code hooks; no skill action needed.
- Stop hook (
scripts/on-stop-done.py) — fires after each response. POSTs/claude-code/notifywithlevel: "done". If 5-hour or 7-day usage is>=usage_threshold, it also POSTs/claude-code/usage. Skipped whentask_done_enabledisfalse. - Notification hook (
scripts/on-notify.py) — fires when Claude needs approval or input has gone idle. POSTs/claude-code/notifywithlevel: "attention". Skipped whennotify_enabledisfalse.
Both honor sounds_enabled for the sound field and fail silently if no device
is connected.
5. Tuning (plain-language config edits)
Let the user retune behavior in plain language — read config, change only the
relevant key(s), preserve everything else, write back with permission 0600,
then confirm in plain language. If the config is missing → tell them to connect a
device first.
5.1 Threshold
When to use: "warn me earlier", "set warning threshold to 60", "only warn me near the limit", "I want pushes at 90%".
- Explicit number → use it. "earlier"/"sooner" → lower (e.g. 60). "only near the limit" → raise (e.g. 90). If unclear, ask for a number.
- Validate integer 0–100, then set
usage_threshold.
5.2 The three toggle flags
All default true and live at the top level of the config.
| Key | Controls |
|---|---|
sounds_enabled |
Whether events carry a sound cue. false = silent feedback. |
task_done_enabled |
The Task Done push after each response. |
notify_enabled |
The "Claude needs you" ping (approval / idle). |
Mapping requests:
- "mute my device" / "turn off the sounds" →
sounds_enabled = false - "unmute" / "sounds back on" →
sounds_enabled = true - "stop the task done notification" →
task_done_enabled = false - "stop pinging me when Claude needs me" →
notify_enabled = false - "turn all notifications off" →
task_done_enabled = falseandnotify_enabled = false - "turn everything back on" → all three
true
The hooks read these flags on every run, so changes take effect immediately — no restart.
6. HTTP responses
| Status | Meaning / Action |
|---|---|
| 200 | Success, body {"ok": true}. Update last_known_ip in config if it changed. |
| 400 | Bad JSON / malformed payload. Fix the payload and retry. |
| Connection error / timeout | Cached IP is stale or device offline. Re-run discovery (section 1) and retry once. |
Important
- Python 3 stdlib only — no pip, no extra dependencies.
- Keep
sounda boolean; the device decides the actual cue. - Never print or log the OAuth token.
- Config file permission must be
0600. - Device-side
/claude-code/notifyand/claude-code/usagereceivers exist and log the payload, but may not yet render device feedback on every device (the HAL bridge is the remaining work);/healthand/statusexist for liveness/discovery.