Small bash scripts on your Mac that poll the APIs Axol can't webhook, read their tokens from Keychain, and POST each new item straight to her loopback listener. Named after the feathery pink plumes axolotls wave to sample the water — same rhythmic reach, same pickup role.
Install a gill →Axol's remote-alerts path is push-style: a service you own a webhook on (your GitHub repo, your Stripe account, your Sentry project) signs a POST, the neuromast queues it, lateral-line pulls it down, Axol's bubble pops. Works cleanly when you can write the webhook URL into the sender's settings.
It doesn't cover the things you want alerts on but don't admin — repos someone else owns that @mention you, review requests across every org you belong to, security advisories on the long tail of repos you watch. GitHub only exposes those through your notifications inbox, and that inbox is poll-only with a personal-access token. Same pattern for a handful of other services (Linear PATs, Google Calendar read-only scopes, RSS feeds).
Gills fill that gap with the inverse data flow: Axol polls the upstream on your behalf and POSTs each new item to her own loopback port. No neuromast, no cloud. The credential never leaves your Mac.
In an axolotl: three pairs of feathery external gills fan out on either side of the head — the single most recognizable thing about the animal. They wave in slow, steady arcs, pulling fresh water past the fronds to sample oxygen and chemical signals from the surrounding column.
In Axol: one bash script per service you want to pull from, running on its own launchd job. Each sample is one full cycle — grab the token, ask the upstream what's new since last time, hand any fresh items to the bubble. Same rhythmic reach, same pickup role.
A neuromast is a sensor that waits for pressure to come to it. A gill is a sensor that actively goes and gets the signal. Pairs cleanly with the existing neuromast / lateral-line vocabulary.
Every gill follows the same shape, enforced by a handful of helpers in lib.sh. A single sample does six things, any one of which can bail the whole pass cleanly without burning credentials or the upstream's rate limit:
flock (with a noclobber-symlink fallback for macOS's flock-less default). Overlapping samples exit silently — whoever got the lock runs alone.
127.0.0.1:47329. If she's closed, bail before calling the upstream so you don't spend rate-limit budget just to throw the result away.
security find-generic-password -s <service> -w. Missing item → log once and exit 0; no retry storm, no stack trace in the launchd log.
~/Library/Application Support/Axol/gills/<name>.cursor. First-run default is the gill's call — github-notifications starts an hour ago so you don't backfill stale unread threads.
POST it to the loopback receiver. Bubbles render through the same adapter pipeline as webhook-originated alerts.
.tmp + mv, so a crash mid-write leaves either the old or the new value — never a truncated string. If any POST failed, leave the cursor alone and retry next sample.
Two commands, assuming you already have Axol running. First time only — stash the credential in macOS Keychain where the gill can find it:
security add-generic-password -a "$USER" -s axol-github-pat -w ghp_xxxxxxxxxxxxxxxxxxxxxxxx
Then install the launchd job:
cd lateral-line/gills
./install.sh github-notifications
That writes ~/Library/LaunchAgents/com.axol.gill.github-notifications.plist pointing at the script, bootstraps it into your user session, and runs a first sample immediately. Default interval is 30 seconds — same cadence lateral-line uses.
Tail the log to watch the cycle:
tail -f ~/Library/Logs/Axol/gill-github-notifications.log
Each line is one sample — delivered=N/M cursor=… on the happy path, a one-word reason on the sad path. Nothing noisy between.
./install.sh github-notifications --uninstall bootouts the launchd job and removes the plist. Your cursor and keychain item stay put — rotation is separate from uninstall.
Shipping with Axol today. Each one lives as a self-contained bash script in lateral-line/gills/.
| Gill | Upstream | Keychain service | Scope required |
|---|---|---|---|
github-notifications |
GitHub /notifications |
axol-github-pat |
Fine-grained PAT, Notifications: read |
github-notifications maps each thread's reason field to an Axol priority — review_requested, mention, team_mention, assign, and security_alert render as urgent (pinned bubble, stays until clicked); everything else (comments, subscribed threads, CI activity) rides the normal auto-dismiss lane. API URLs are rewritten to their human-facing github.com equivalents on the way out, so clicking the bubble opens the PR / issue in the browser, not a raw JSON blob.
A gill is any bash script that sources lib.sh, calls gill_init first, and uses the six helpers the lib exposes. No registry file, no manifest — install.sh <name> just looks for <name>.sh next to it.
Minimum viable gill, end-to-end:
#!/usr/bin/env bash
set -euo pipefail
. "$(cd "$(dirname "$0")" && pwd)/lib.sh"
gill_init "example"
gill_axol_up || exit 0
token=$(gill_keychain_secret "axol-example-token") || exit 0
since=$(gill_cursor_read)
resp=$(curl -fsS -H "Authorization: Bearer $token" \
"https://api.example.com/items?since=$since")
echo "$resp" | jq -c '.items[]' | while read -r item; do
title=$(jq -r '.title' <<<"$item")
env=$(jq -n --arg t "$title" '{title:$t, body:"new item", icon:"bell"}')
gill_post_envelope "$env" || exit 0
done
gill_cursor_write "$(date -u +%FT%TZ)"
That's it. ./install.sh example writes a launchd plist for it automatically. The helpers you get:
| Helper | Does |
|---|---|
gill_init <name> | Claims a per-gill file lock; sets up cursor + log context. |
gill_axol_up | TCP probe of 127.0.0.1:47329; returns non-zero if Axol's closed. |
gill_keychain_secret <svc> | Prints the password for Keychain service <svc>, or returns non-zero + logs when missing. |
gill_cursor_read / gill_cursor_write <val> | Per-gill cursor I/O, atomic on write. |
gill_post_envelope <json> | POST a pre-rendered envelope to Axol's loopback. |
gill_log <msg> | Timestamped line to the gill's launchd log. |
zz-generic.json) fires for anything with a title, so a minimal gill doesn't need its own matching JSON file. Write a dedicated adapter only if you want a different rendering per subtype inside one source.
Gills are intentionally local. That shapes the security posture:
security(1) at each sample and nothing else sees the password.StartInterval in the plist.github-notifications asks for Notifications: read and nothing else — if the token leaks, the blast radius is "someone can see what's in your GitHub notifications inbox," not "someone can push to your repos."security delete-generic-password -s axol-github-pat && security add-generic-password -a "$USER" -s axol-github-pat -w <new-token>. No redeploy, no restart; the next sample picks it up.