@trygocode/notify
v0.6.4
Published
Free phone + branded desktop notifications for any coding agent (Cursor, Claude Code, OpenCode, Ralph/Homer) via the GoCode app.
Maintainers
Readme
@trygocode/notify
Free phone and branded desktop notifications for any coding agent — Cursor, Claude Code, OpenCode, or any Ralph/Homer loop — delivered to your phone via the GoCode app + FCM, and to your computer as a native "GoCode" banner.
You get an automatic notification when your agent finishes a turn, goes idle waiting for you, errors out, or when an overnight loop completes / halts — on your phone AND on the computer you're working on (on by default) — so you can walk away and let either device tell you when it needs you.
Contents
- Install — two equally-supported paths
- Pairing — step by step
- The three triggers
- Hand off to your server —
launch/autopilot - Auto-push to git (opt-in)
- Settings sync — the
configcommand - Ralph/Homer opt-in snippet (trigger C)
- Notify from any custom script (the minimal one-liner)
- Troubleshooting
- Changelog
- Develop
- Layout
Install — two equally-supported paths
Both paths converge on the same installer (gocode-notify setup): it pairs this
machine, auto-detects your agent runtimes, and merges the hooks + MCP server +
anti-double-ping rule into each one's config (never clobbering your existing
settings; safe to re-run).
Path 1 — paste a one-liner into your terminal
npx @trygocode/notify@latest setupThis runs the interactive installer: it prompts for the 6-digit pairing code
(from the GoCode app → "Connect a coding agent"), then detects and configures
Claude Code / Cursor / OpenCode. Re-run any time — it's idempotent; pass
--force to re-pair.
Until the package is published to npm, point
npxat the local tarball (npm packin this directory, thennpx ./gocode-notify-*.tgz setup) or a path install. The npm publish is a user-owned step — seedocs/GOCODE_NOTIFY_MANUAL_STEPS.md.
Path 2 — paste a prompt into your AI agent and let it install
Hand this to Cursor / Claude Code and the agent does the install for you. The
--agent-driven flag suppresses interactive prompts and emits one JSON line per
step so the agent can verify each one:
Install GoCode phone notifications for this machine. Run:
npx @trygocode/notify@latest setup --agent-driven --pair-code <CODE>
Then confirm the hooks and MCP server were written, and run
npx @trygocode/notify@latest test
to send a test push to my phone. Report whether the test push arrived.Replace <CODE> with the 6-digit code from the GoCode app. --agent-driven is
fully idempotent and machine-readable; it never spawns a prompt.
Pairing — step by step
A dev machine running agent hooks has no GoCode login (no JWT), so it can't use GitHub OAuth. Instead you pair it once with a short-lived 6-digit code, which the CLI exchanges for a scoped, push-only API key stored locally. The key can only send pushes to your phone — it can't read chats, settings, or trigger any agent action, and you can revoke it from the app at any time.
In the GoCode app, open Settings → "Connect a coding agent". The app shows a large 6-digit code (valid 10 minutes), a copyable
npx @trygocode/notify login --code 123456line, and a 10:00 countdown. Tap "Generate new code" if it expires.On the machine, either run the full installer (
npx @trygocode/notify@latest setup, which pairs and writes your agent configs) or just pair on its own:# Interactive — prompts for the code: npx @trygocode/notify@latest login # Or pass it directly (and optionally label this machine): npx @trygocode/notify@latest login --code 123456 --label "MacBook Pro — Cursor"The CLI calls the server's
pair/claimendpoint, receives the API key once, and writes it to~/.gocode/credentials(chmod600). The app flips to a "connected ✓" success state showing the machine's label.Verify the round-trip with a real push to your phone:
npx @trygocode/notify@latest testA "GoCode test" notification should arrive on your paired device. If it doesn't, see Troubleshooting.
Re-pairing. Both login and setup are idempotent — re-running them won't
clobber an existing pairing. To deliberately replace the stored key (new machine
owner, rotated key), pass --force to setup (or just run login again with a
fresh code). Revoke an old machine from the app's "Connected agents" screen.
Server selection. Pairing and every send resolve the server URL in this
precedence order: the --server flag → the GOCODE_SERVER env var → the value
saved in ~/.gocode/credentials → the built-in default
(https://oh.jeltechsolutions.com). You only need --server for a self-hosted
or staging GoCode server.
Redistributing under your own server. The built-in default is not baked in
as a fixed author box — set GOCODE_DEFAULT_SERVER to point fresh, unpaired
installs at your OWN GoCode server without patching the source or having every
user pass --server/GOCODE_SERVER. It only changes the fallback default;
already-paired machines keep the server in their ~/.gocode/credentials.
The three triggers
| Trigger | Mechanism | Fires when |
|---|---|---|
| (A) Runtime hook | Cursor stop / Claude Code Stop+Notification / OpenCode session.idle plugin | The MAIN agent finishes a turn, goes idle, or errors — automatic, the killer feature |
| (B) MCP tool | gocode_notify tool the agent calls | You explicitly ask "ping me when X is done" mid-task |
| (C) Loop shell hook | one line in your loop's completion/halt path | A Ralph/Homer loop reaches completed / halted |
The installed rule/skill tells the agent not to call the MCP tool for done/idle/error pings — those are owned by the deterministic hook (A), so you never get double-pinged.
Cursor
stop.status→ kind mapping (since 0.3.0;abortedcorrected 0.4.0). Cursor'sstophook carries astatusfield in its stdin JSON. Theon-stopdispatcher maps it to the right notification kind automatically:| Cursor
stop.status| Notification kind | Meaning | |---|---|---| |completed|finished| Agent turned cleanly (default — same as before 0.3.0) | |aborted| (suppressed — no notification) | You pressed Stop to interrupt the turn — a manual stop sends nothing | |error|error| Agent hit an error | | absent / unrecognised |finished| Back-compatible fallback |Before 0.3.0, Cursor only ever sent
finishedregardless of what the agent did. The status mapping requires no hook re-installation — rungocode-notify setup --forceonce to apply the updated hook command.
Question detection — "Agent needs you" (since 0.5.0). When the agent pauses to ask you a question (as opposed to finishing its work), the package fires an
awaiting_inputping titled "Agent needs you" instead of a misleadingfinished. The available signal differs per runtime:| Runtime | Question signal | Status | |---|---|---| | Claude Code | dedicated
Notificationhook →awaiting_input| ✅ Live — questions show as questions | | OpenCode |permission.askedplugin event →awaiting_input| ✅ Live — OpenCode genuinely exposes a "needs you" signal | | Cursor |postToolUsehook scoped to theAskQuestiontool →awaiting_input| ⏳ Installed but dormant — see note |Cursor caveat: Cursor currently exposes no "agent needs input" event (its docs list
Notificationas unsupported), and there is a confirmed-open Cursor bug where theAskQuestiontool fires zero hooks. So a Cursor question is indistinguishable from a completion at the only signal we get (thestophook), and you'll receivefinisheduntil Cursor ships their fix. We install thepostToolUse(AskQuestion)hook now so that the moment Cursor fixes the bug, question detection auto-activates with no new install for everyone already running@trygocode/notify@latest. Rungocode-notify setup --forceonce to wire it in.
OpenCode's hook (A) is a small
session.idleplugin written to~/.config/opencode/plugin/gocode-notify.js; on eachsession.idleevent it fire-and-forgetsgocode-notify on-stop --source opencode(the same dispatcher as Cursor/Claude), so OpenCode is a fully-supported runtime with identical behaviour.
Under the hood (since auto-push landed): the runtime hook (A) no longer shells straight to
send. It now calls a single end-of-turn dispatcher,gocode-notify on-stop --source <runtime> || true, which decides per repo: if auto-push is enabled for this repo it runs the push flow (and sends ONE notification carrying the commit summary); otherwise it sends the ordinaryfinishedping exactly as before. One hook entry, one notification per turn. Existing installs upgrade their hook automatically on the nextsetup/setup --force.sendandtestremain callable on their own for scripts and power users.
Desktop notifications (branded, on by default)
GoCode Notify also raises a real, branded "GoCode" banner on the computer you're working on — the same instant your phone gets pinged — so you're told whether you're at your desk or away. It's ON by default once the package is installed and is controlled by the same server-synced settings as the phone push, so a toggle in the app or the terminal flips the computer banner everywhere.
Word-for-word identical to the phone. The banner reads exactly the same as the
push you'd get on your phone — same status emoji, IDE/source label, project, and
per-kind copy (e.g. ✅ Cursor · openhandapp — Agent finished). The wording is a
faithful mirror of the server's notification decoration and the Flutter client's,
pinned by a cross-check test (notify_copy.test.ts) so the three surfaces can't
drift.
Click to open your IDE (macOS). Clicking the banner opens the IDE window for that project — the same editor that raised the turn (Cursor / VS Code), focused on the project you were working in. This is best-effort: when the IDE/project can't be named the banner is simply display-only (never a dead or mis-routed click). It opens the project window, not a specific past chat — IDEs don't expose reliable per-chat deep links.
| OS | How it's shown | Branded as |
|---|---|---|
| macOS | A one-time osacompiled GoCode.app helper under ~/.gocode/desktop/ raises the banner via the system | "GoCode" + the GoCode icon |
| Windows | A WinRT ToastGeneric toast bound to a one-time Start-Menu shortcut carrying the GoCode.Notify AppUserModelID | "GoCode" |
| Linux | notify-send -a GoCode -i <icon> | "GoCode" + icon |
Zero new dependencies. It uses only tools that ship with the OS (osacompile/
osascript/sips on macOS, PowerShell + WinRT on Windows, notify-send on Linux)
— so the one-line install does not change at all. The first banner builds a tiny
branded helper once and caches it; every banner after that just fires it.
Control it
From the GoCode app (Settings → GoCode Notify → Desktop notifications), or from the terminal:
gocode-notify config set desktop.enabled false # silence the computer banner
gocode-notify config set desktop.sound false # keep the banner, drop the soundThe phone push is unaffected — desktop.* only governs the local banner.
Try it right now
# Fire a branded test banner on THIS computer (independent of the phone push):
gocode-notify test --desktopYou should see a "GoCode" notification appear on your screen. On a headless
box / CI / SSH session where a banner makes no sense, set
GOCODE_NOTIFY_NO_DESKTOP=1 to hard-disable local banners for that machine.
macOS branding note. A bare
osascriptbanner is attributed to "Script Editor". To brand it "GoCode", the package compiles a minimal AppleScript app bundle (~/.gocode/desktop/GoCode.app) once withosacompile(ships with every Mac — no Xcode/Swift) and drops the GoCode icon into it viasips. If that one-time setup ever fails, the banner still fires (unbranded) rather than blocking your turn.
Hand off to your server — launch / autopilot
New in 0.2.0. Notify can now do more than ping your phone — it can hand a big task off to your GoCode server to run as an autonomous Autopilot loop. Close your laptop; the work keeps going server-side in your isolated sandbox, shows up in the GoCode phone app like any other Autopilot run, and your phone buzzes when it's done (or needs you).
This is the opposite direction of the three notify triggers above: instead of
your local agent pinging your phone, launch starts a fresh server-side
build from an explicit, self-contained task. It reuses the same gck_
pairing — no new login. It reads no conversation content (only the task
string you hand it), so it ships in this base package (see the trust note below).
CLI
# Start a loop from a plain task (server synthesizes a PRD, then builds):
gocode-notify launch "Refactor the auth module to use sessions, add tests" \
--repo owner/repo
# alias:
gocode-notify autopilot "Add a CSV export endpoint with tests" --repo owner/repo
# Or hand it a pre-written checkbox PRD instead of a task string:
gocode-notify launch --prd-file ./my-prd.md --repo owner/repo| Flag | Meaning |
|---|---|
| <task> (positional) | Plain-language task the server loop builds. Mutually exclusive with --prd-file. |
| --prd-file <path> | A pre-written checkbox PRD (base64-encoded and sent as prd_markdown). |
| --repo owner/repo | Optional repo context for synthesis / the loop's target. |
| --branch <suffix> | Optional branch suffix the loop pushes to. |
| --model "Profile" | Optional LLM-profile name override. |
| --runner-kind <kind> | Optional runner override (default openhands-conversation). |
| --server <URL> | Same server-resolution precedence as every other command. |
| --agent-driven | Emit one machine-readable JSON step line (for installers/agents). |
On success it prints the loop id, a deep link that opens the run in the
app, and a "running on your GoCode server — close your laptop; your phone will
buzz when it's done" line, then exits 0. Unlike the fire-and-forget
send/on-stop hooks, launch is an explicit interactive action: it
reports real failures (not paired / unreachable / 4xx) with a clear message and a
non-zero exit, and it does not queue to the offline outbox (a stale
launch surfacing hours later could duplicate work).
MCP tool
Your desktop agent (Cursor / Claude Code / OpenCode) can fire a remote loop
mid-session via the third MCP tool, gocode_launch_autopilot
({ task (required), repo?, branch? }). Its description tells the agent to call
it only when you explicitly ask to offload / run in the background / overnight
— never for a normal task it can do right there. On success it relays the loop id
- deep link; on failure it returns an actionable re-pair hint.
Heads-up the rule states: the server loop is a fresh agent with no access to your IDE's open/unsaved files — so the task must be self-contained and point at a repo the server can build from a clean checkout. If the work depends on local uncommitted state, commit/push first, then hand it off.
Trust boundary
launch reads only the explicit task string (and optional repo/branch) you
hand it — it does not read or upload your IDE conversation transcript, and it
touches only ~/.gocode/credentials (the gck_ key it already uses for notify).
It POSTs to your own configurable GoCode server. So the base package's "never
reads your conversation content" promise still holds.
Auto-push to git (opt-in)
GoCode Notify can auto-commit and push your work after every agent turn — with an AI-written commit message describing what just changed — so an overnight Cursor/Claude/Ralph session lands as a tidy series of pushed commits and your phone tells you each time, commit summary included.
It is OFF by default and deliberately conservative (see the safety posture
below). The full design lives in
docs/GOCODE_NOTIFY_AUTO_PUSH_AND_SYNC.md.
Enable it
From the GoCode app (Settings → GoCode Notify → Auto-push after every prompt; a first-enable dialog explains the behaviour), or from the terminal:
# Turn auto-push on for ALL repos (global default):
gocode-notify config set auto_push.enabled true
# Or scope it to just the current repo:
gocode-notify config set auto_push.enabled true --repoEither editor writes to the server (the single source of truth); the change is
picked up by every linked machine on its next turn (within the 60s settings
cache, or immediately after gocode-notify config pull).
What happens on each turn (when enabled)
The on-stop dispatcher runs this best-effort push flow (it always exits 0
so a git hiccup can never block or fail your agent's turn):
- Resolve the merged Notify settings for this repo (cached ~60s).
- If
auto_push.enabledis not true for the repo → no-op (plain ping). - Verify it's a git repo; resolve the target branch (per-repo override → global default → current checked-out branch).
- Branch guard: never push to
main/masterunless you have separately setauto_push.allow_protected true. Protected + not allowed → skip, no push. git add -A; if the tree is clean → exit 0 (no spurious notification).- Compose the commit message (AI, with deterministic fallback — see below).
git committhengit push <remote> <branch>— fast-forward only, never--force. A non-FF rejection sends anerrornotification ("auto-push rejected — pull + retry") and exits 0.- Send ONE
finishedpush whose body carries the commit summary, branch, and short SHA.
The AI commit message (free) + deterministic fallback
The commit subject/body is summarised using credentials you already have on the machine — so it costs GoCode nothing. Resolution chain (first that works wins, hard 8s cap):
commit_message.mode = "deterministic"→ skip AI, use the template below.- Otherwise an explicit
commit_message.command(e.g."claude -p","cursor-agent --print","ollama run llama3.2") — the staged diff is piped in on stdin with a fixed prompt; output is sanitised (code fences / "Here is…" preambles stripped, subject clamped to ≤72 chars). - Auto-detect from
--source:claudeon PATH for Claude Code,cursor-agentfor Cursor, elseollamaif present. - Deterministic fallback (always available — used if the above fail or time
out):
<type>: <N> file(s) changed on <branch>+ agit diff --statblock, where<type>is inferred from the changed paths (docs:/test:/chore:/feat:). The push never fails because the AI summary was unavailable.
Try it without writing anything
# Print exactly what the end-of-turn dispatcher WOULD do (no git writes, no push):
gocode-notify on-stop --dry-runSafety posture (conservative defaults)
- OFF by default, opt-in, with a first-time confirmation in the app.
- Branch guard ON:
main/masterare never auto-pushed unlessauto_push.allow_protectedis explicitly true. - No force-push, ever — plain fast-forward
git push; non-FF → error notification, no rewrite. - Never blocks the agent — every step is best-effort and exits 0, the same
contract as
send.
Settings sync — the config command
Notify behaviour used to be hard-wired; now it's a small set of per-user settings stored on the server and editable from either the phone app or the terminal. Whatever you change in one place propagates to the other (and to your other linked machines).
gocode-notify config get [--repo] # print the merged settings (JSON/pretty)
gocode-notify config set <key> <value> [--repo] # validate, write to server, refresh cache
gocode-notify config pull # force-refresh the local cache from server--reposcopes a read or write to the current repo's per-project override (keyed by a stablerepo_keyderived from theoriginremote) instead of your global defaults. Per-repo overrides deep-merge over the global blob.- Writes go to the server and update the local cache so the next stop-hook is immediate. Unknown keys are rejected with the list of valid keys.
Valid keys (mirror of the canonical schema):
| Key | Type | Meaning |
|---|---|---|
| kinds.finished / kinds.error / kinds.awaiting_input / kinds.loop_completed / kinds.loop_halted / kinds.ralph_waiting | bool | Per-kind notification toggles |
| min_duration_seconds | int ≥ 0 | Only notify if the turn ran ≥ N seconds (0 = always) |
| quiet_hours.enabled / quiet_hours.start / quiet_hours.end / quiet_hours.tz | bool / HH:MM / HH:MM / IANA tz | Do-not-disturb window |
| auto_push.enabled | bool | Master auto-push switch (OFF by default) |
| auto_push.default_branch | string | null | Global default push branch (null = current branch) |
| auto_push.branch | string | null | --repo only — per-repo push branch override |
| auto_push.allow_protected | bool | Permit auto-push to main/master (default false) |
| auto_push.remote | string | Git remote to push to (default origin) |
| auto_push.skip_git_hooks | bool | Pass --no-verify to the auto-commit (default false) |
| commit_message.mode | auto | ai | deterministic | Commit-message strategy |
| commit_message.command | string | null | Explicit local summariser command |
| commit_message.max_diff_bytes | int > 0 | Diff byte cap fed to the summariser (default 61440) |
| desktop.enabled | bool | Branded desktop banner on this computer (ON by default) |
| desktop.sound | bool | Play a sound with the desktop banner (default true) |
# Examples
gocode-notify config set quiet_hours "23:00-07:00" # shorthand: enable + set window
gocode-notify config set min_duration_seconds 30
gocode-notify config set kinds.error false
gocode-notify config set auto_push.branch dev --repo # push this repo to "dev"Ralph/Homer opt-in snippet (trigger C)
For power users running a loop they control (this repo's ralph/homer
skills, a while :; do … done one-liner, or any custom driver), drop these two
lines into the loop's completion/halt path:
# At loop completion:
gocode-notify send --kind loop_completed --source ralph --project "$(basename "$PWD")" || true
# At loop halt (paused_max_failures / awaiting_human):
gocode-notify send --kind loop_halted --source ralph --project "$(basename "$PWD")" \
--title "Ralph halted — needs you" || trueThe ready-to-copy version with comments lives at
snippets/ralph-homer.sh.
This is opt-in and never auto-injected — the installer does not edit your
loop scripts. Both lines are fire-and-forget (|| true + the CLI's 5s
self-timeout), so a failed or slow push can never block or fail your loop.
Notify from any custom script (the minimal one-liner)
Any shell script — a cron job, a Makefile target, a post-build hook, or
someone else's automation — can send a push with a single line, no configuration
beyond a one-time gocode-notify login pairing:
gocode-notify send --kind finished --source <name> || trueReplace <name> with a short identifier for the script (e.g. ci, build,
deploy). The || true guard ensures a failed or slow push never blocks the
calling script (the CLI also self-times-out in 5 seconds).
Discovery hint:
gocode-notify statusalways prints this line at the bottom of its report so you can copy-paste it even when you don't have the README handy.
Other useful kinds for custom scripts:
| Kind | When to use |
|---|---|
| finished | Script completed cleanly |
| error | Script hit an error |
| awaiting_input | Script paused; a human is needed |
| loop_completed | Long-running loop finished all work |
| loop_halted | Long-running loop stopped; human needed |
Pass --title "My script" and --body "extra detail" to customise the
notification text. Use --project "$(basename "$PWD")" to badge it with the
project name on your phone. All flags are optional — only --kind is required.
See snippets/ralph-homer.sh for the full Ralph/Homer lifecycle pattern
(completed / halted / stall-edge / resumed).
Troubleshooting
Start here: gocode-notify status prints a one-screen report — whether
credentials are present (and the bound user/label), whether the server is
reachable, which agent runtimes were detected, and whether each one's config has
been written. Most issues below are diagnosable from that output.
| Symptom | Likely cause & fix |
|---|---|
| test / send prints "not paired" | No ~/.gocode/credentials. Run gocode-notify login and pair from the app (see Pairing). |
| Pairing fails ("invalid or expired code") | Codes expire after 10 min and are single-use. Tap "Generate new code" in the app and re-run login with the fresh code. |
| status shows Server: not reachable | Network/DNS/firewall, or a wrong server URL. Confirm you can reach https://oh.jeltechsolutions.com; check the --server flag / GOCODE_SERVER env / the server field in ~/.gocode/credentials. |
| No push arrives even though test exits 0 | The send is fire-and-forget and exits 0 even on failure — check ~/.gocode/notify.log for the real error. Also confirm push permissions are granted in the GoCode app and the device token is registered (re-open the app once after signing in). |
| Double pings (two notifications per event) | The agent is calling the gocode_notify MCP tool and the runtime hook is firing. Re-run setup so the anti-double-ping rule/skill is installed; it tells the agent not to notify for automatic done/idle/error events. |
| Hook doesn't fire in Cursor / Claude Code / OpenCode | Re-run setup and check status shows "config written" for that runtime. Restart the agent app so it reloads ~/.cursor/hooks.json / ~/.claude/settings.json / ~/.config/opencode/plugin/gocode-notify.js. The hooks are merged, never clobbered — your existing hooks are preserved. |
| Pushes queue up while offline then arrive later | Expected. Sends made while the server is unreachable are enqueued to ~/.gocode/outbox/ (size-capped, drop-oldest) and flushed best-effort on the next send. A missed "done" ping is acceptable; a blocked agent is not. |
| npx @trygocode/notify can't find the package | Until it's published to npm, run from a local tarball: npm pack in tools/gocode-notify/, then npx ./gocode-notify-*.tgz <command>. See docs/GOCODE_NOTIFY_MANUAL_STEPS.md. |
| Want it gone | gocode-notify uninstall removes exactly the hook/MCP/rule entries this tool added (nothing else). Delete ~/.gocode/ to also drop the stored credentials, and revoke the key from the app's "Connected agents" screen. |
Logs & files. Failures are appended to ~/.gocode/notify.log (size-capped,
rotated to notify.log.1). Credentials live in ~/.gocode/credentials (chmod
600); non-secret prefs in ~/.gocode/config.json; the offline queue in
~/.gocode/outbox/.
For the device/secret/publish/deploy steps that are not part of this package
(Android google-services.json, iOS APNs, server service-account, WSL rebuild,
npm publish, real-device E2E), see
docs/GOCODE_NOTIFY_MANUAL_STEPS.md.
Changelog
0.6.1
- Banner now reliably POPS top-right (not just history). The signed helper
posts the notification then exits ~150ms later so macOS does BACKGROUND
delivery (the system shows the banner per the user's settings). The previous
attempt to force foreground delivery via
willPresentsuppressed the banner to Notification Center history — verified fixed live on macOS 26.4. - Surface the underlying
requestAuthorizationerror in the helper's JSON result (detail/authError) instead of collapsing to a bare "not authorized". - Extract the post-
add()exit delay to a documented named constant.
0.6.0
- Real native macOS "Allow Notifications" dialog (signed helper). GoCode now
ships a tiny Developer-ID-signed macOS app bundle (
GoCodeNotifier.app, Team88DC7KFY7M) inside the package. On macOS,gocode-notify permissions(and the post-setupnudge) now pop the real system Allow/Don't-Allow permission modal — the thing a plainnpx/osascripthelper can't do — instead of only opening System Settings and asking you to hunt for the toggle. Once allowed, desktop banners post as a first-class, authorized app (proper attribution + a real click-to-open-your-IDE action).- How it ships: the bundle is pre-signed at build time by the
maintainer (
native/macos/build_and_sign.sh) and committed toassets/, so end-users need no Xcode, no Developer ID, no compile step — the one-line install is unchanged. The signature survives the npm pack/extract round-trip. - Two macOS rules it honors (both required for the dialog to appear): the
helper is copied to a stable location (
~/.gocode/desktop/) — never/tmp— and is always launched via LaunchServices (open), never by exec'ing the inner binary (direct-exec auto-denies and the denial sticks). - Hot-path stays fast: fire-and-forget banners read a cached auth status
(
~/.gocode/desktop/.auth-status), so a notification never does a slow live permission round-trip. The signed banner is used only once you've granted; otherwise it transparently falls back to the legacy applet banner. - Own identity: the signed helper uses bundle id
com.gocode.notify.helper(distinct from the legacy applet'scom.gocode.notify.desktop) so it owns its own row in System Settings → Notifications. - Linux/Windows unchanged. This is a macOS-only enhancement; other platforms keep their existing branded-banner paths.
- How it ships: the bundle is pre-signed at build time by the
maintainer (
0.5.0
- Question detection across ALL IDEs — "Agent needs you" vs "finished". When
the agent pauses to ask you a question, you now get an
awaiting_input("Agent needs you") notification instead of a misleadingfinished, on every supported runtime that exposes a question signal:- Claude Code — already wired (
Notificationhook); unchanged. - OpenCode — NEW: the plugin now listens to
permission.asked(the moment the agent raises a request) and firesawaiting_input. OpenCode genuinely exposes a "the agent needs the user" signal via permission events. (We listen topermission.askedonly, notpermission.updated/replied, which also fire after the user answers.) - Cursor — NEW (dormant): installs a
postToolUsehook scoped to theAskQuestion/AskUserQuestiontool. Cursor has a confirmed-open bug where that tool fires zero hooks, and Cursor exposes noNotificationevent, so the hook is dormant today — but it auto-activates with no new install the moment Cursor ships their fix. Until then, Cursor keeps itsfinishedfallback.
- Claude Code — already wired (
- Corrected the documented Cursor
abortedmapping. The README table previously (incorrectly) showedaborted → awaiting_input; the actual behaviour (since the 0.4.0 fix) is that a manual Stop press (aborted) sends no notification at all. A user force-stop is now clearly distinguished from a genuine IDE question. - Run
gocode-notify setup --forceonce to wire the new hooks into Cursor + OpenCode.
0.4.0
- New: branded desktop notifications (on by default). Every event that pings
your phone now also raises a native, GoCode-branded banner on the computer
you're working on — macOS (a one-time
osacompiledGoCode.apphelper +sipsicon), Windows (a WinRTToastGenerictoast bound to a one-time Start-Menu shortcut with theGoCode.NotifyAppUserModelID), and Linux (notify-send -a GoCode). Zero new dependencies — OS-native tools only, so the one-line install is unchanged. Controlled by the same server-synced settings as the phone push via the newdesktop.enabled/desktop.soundkeys (both default ON); toggle from the app orgocode-notify config set desktop.enabled false. Try it withgocode-notify test --desktop. Hard- disable per machine withGOCODE_NOTIFY_NO_DESKTOP=1(headless / CI / SSH). Best-effort + non-blocking: a failed banner never blocks or fails your turn. - Word-for-word match with the phone push. The desktop banner now reads
EXACTLY like the phone notification — same status emoji + IDE/source label +
project + per-kind title/body (e.g.
✅ Cursor · openhandapp — Agent finished). Newsrc/notify_copy.tsmirrors the server's_decorated_title/_decorated_body+_DEFAULT_TITLES/_DEFAULT_BODIESand the Flutter client'sdecorateNotificationTitle/sourceLabelFor, pinned by a cross-check test so the CLI/server/app surfaces can't drift. - Click to open the IDE window for the project (macOS). Clicking the banner
opens the IDE that raised the turn (Cursor / VS Code) focused on the project
directory. Implemented zero-dependency: the branded AppleScript applet handles
the click re-run (documented macOS behaviour) and
open -a <ide> <project>. Best-effort — display-only when the IDE/project can't be named. Opens the project window, not a specific chat (IDEs lack reliable per-chat deep links).
0.3.0
- New: Cursor
stop.status→ kind mapping (T-CUR1/T-CUR4). Theon-stopdispatcher now reads the Cursorstophook's stdin JSON and mapsstatusto the right notification kind:completed→finished,aborted→awaiting_input,error→error. Back-compatible: absent/unrecognised status →finished. Rungocode-notify setup --forceto pick up the updated hook command. - New:
ralph_waiting+ralph_resumedkinds (T-C1).NOTIFY_KINDSnow includes the Ralph/Homer offline-stall lifecycle kinds.ralph_waitingfires once on the stall edge (server drops repeats until a recovery event re-arms it);ralph_resumedis a silent control event that resets the stall state machine. Seesnippets/ralph-homer.shfor the edge-trigger pattern. - Kind taxonomy (T-S1). The server now classifies every kind into push-worthy
vs. silent-info buckets:
ralph_question/ralph_advancedare never pushed (Oracle-answerable questions are silent);ralph_halted/ralph_completed/ralph_waiting(edge) are the only loop kinds that reach FCM. - Foreground suppression (T-S2/T-S3). The server suppresses a push when the
app is open on that exact chat (
POST /api/v1/notify/presenceheartbeat, 45s TTL). Different-chat or closed app → push goes out as normal. - Offline/stall debounce (T-S4). Server-side edge-triggered state machine: a loop retrying every 60s on a quota outage pushes once on stall and stays silent until it recovers and stalls again.
0.2.0
- New:
launch/autopilotcommand — hand a large, multi-step task off to your GoCode server to run as an autonomous Autopilot loop. Reuses the existinggck_pairing; prints a loop id + app deep link; explicit failures exit non-zero (no offline queueing). See Hand off to your server. - New:
gocode_launch_autopilotMCP tool — the third tool on the stdio MCP server, so a desktop agent can fire a remote loop mid-session (explicit offload only). The installed Cursor rule / Claude skill / OpenCode snippet teach the agent when to offload and that the server loop is a fresh, self-contained agent with no access to local files. - App monitoring — IDE-launched loops are discovered by the phone app
(
GET /api/v1/ralph/active), badged "Launched from ", and deep-link from the completion/halt push into the run — full parity with app-launched loops. - Fix:
--versionnow reports the real package version.src/version.tshad drifted (published0.1.6reported0.1.3); it is now kept in lockstep withpackage.json, and the publish workflow refuses to ship a mismatch.
0.1.x
- Phone notifications for any coding agent via three triggers (runtime hook, MCP tool, loop snippet), opt-in auto-push to git with an AI-written commit message, and server-synced settings.
Develop
cd tools/gocode-notify
npm install
npm run build # compile TypeScript -> dist/
npm test # builds, then runs node --test on dist/test/
npm run typecheck # type-check only, no emitZero runtime dependencies beyond the MCP SDK (Node built-in fetch/fs/
readline for everything else). Tests use Node's built-in test runner
(node:test).
Layout
| Path | Purpose |
|---|---|
| src/cli.ts | gocode-notify bin entrypoint + command dispatcher (incl. on-stop, config) |
| src/setup.ts | Installer orchestration (pair → detect → write configs) |
| src/claude.ts / src/cursor.ts / src/opencode.ts | Per-client config writers (hooks + MCP + rule/skill); hooks call the on-stop dispatcher. OpenCode uses a session.idle plugin instead of a hooks file |
| src/send.ts / src/login.ts / src/mcp.ts | Core send, pairing, and MCP server (incl. gocode_launch_autopilot) |
| src/desktop_notify.ts | Branded native desktop banners (macOS GoCode.app / Windows WinRT toast / Linux notify-send); zero-dep, best-effort; macOS click-through opens the IDE window |
| src/notify_copy.ts | Banner wording mirror — reproduces the server/Flutter title/body decoration so the desktop banner reads word-for-word like the phone |
| assets/gocode-icon.{png,ico} | Bundled GoCode icon used to brand desktop banners |
| src/launch.ts | Shared launch() core behind the launch/autopilot command + the MCP tool |
| src/push.ts | Auto-push flow behind the on-stop dispatcher — git add/commit/push, FF-only |
| src/commit_message.ts | AI commit-message resolution chain + deterministic fallback |
| src/config.ts | config get\|set\|pull — the dev-machine settings editor (server-synced) |
| src/settings.ts | Canonical Notify settings schema + valid-key specs (mirrors the server + app) |
| snippets/ralph-homer.sh | Opt-in loop completion/halt snippet (trigger C) |
| test/ | node:test smoke + unit tests |
