mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 15:09:42 +02:00
Merge-time fixes for the webhook notification channel (ntfy, Slack, Discord, generic JSON).
Minor 1, App Settings Save silently dropped webhook edits: the modal's main Save now
persists the webhook group beside the settings PUT, the same way it already saves the
model config (saveModelConfigFromSettings), but only when the group differs from what
loadWebhook() put on screen (_webhookPending), so an untouched group never re-PUTs. A
refusal (bad URL, enabled with no URL) shows a warning toast, keeps the modal open and
scrolls to the group with the pasted URL still in the box, instead of a success toast.
Send test now saves pending edits first, so it never tests the old URL while the box
shows a new one. The row says so in one line.
Minor 2, no test for the server.ts glue: new test/webhook-push-glue.test.ts drives the
private sendPushNotifications on a real (never started) WebServer with an EMPTY push
store and webhook.json in the instance data dir, delivering through the real
egress-guarded fetch to a local receiver: a permission prompt arrives with the
host-prefixed ntfy Title and body while Web Push is never called, an immediate repeat is
deduped, "response complete" is skipped under scope attention and sent under all, and a
disabled config or a non-push event sends nothing. Verified it fails when the webhook
call is moved below the "no subscriptions" return.
Minor 3, docs: webhook.json added to CLAUDE.md State Files; a Webhooks section in
docs/wiki/Notifications-And-Approvals.md (setup, what is sent, the secret URL, public
ntfy topics, local targets allowed, dedupe, instance-wide reach in multi-user mode) plus
a table row, and a line in Settings-Reference; new section 10c in
docs/security-architecture.md for the second outbound channel through the web-tab
egress guard.
Nits:
- Orphaned JSDoc: the webhook schema moved below the push schemas, so
PushSubscribeSchema has its comment back.
- Duplicated enums: WebhookUpdateSchema uses z.enum(WEBHOOK_KINDS/WEBHOOK_SCOPES), so
the schema cannot accept a kind the store would coerce away.
- describeError classifies egress refusals with isEgressBlockedError (the
CODEMAN_EGRESS_BLOCKED code anywhere in the cause chain) instead of a message regex;
tests pin a deep cause chain and that matching words alone are not a refusal.
- Markup: the URL input uses set-input, the whitespace-only line is gone, and the switch
row hints to pick a long random topic on public ntfy.sh.
- Remove a saved URL: a "Remove URL" button (shown only while a URL is saved, with a
confirm) sends { url: "", enabled: false }.
- Types placement: WEBHOOK_KINDS/SCOPES and WebhookKind/Scope/Urgency/Config/Result/Status
moved to src/types/push.ts (the IO-side WebhookMessage/Request/Fetch stay in the module).
Browser test extended: main Save persists a pending edit, a refused URL keeps the modal
open with the URL, Send test saves a newly pasted URL first, Remove URL clears it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
226 lines
12 KiB
Markdown
226 lines
12 KiB
Markdown
# Notifications and Approvals
|
|
|
|
An agent that stops to ask a question, with nobody watching, is a run that quietly wasted an
|
|
hour. This page covers every way Codeman tells you it needs you, and how to answer without
|
|
opening the session.
|
|
|
|
## The signals, cheapest first
|
|
|
|
| Surface | Reaches you | Default |
|
|
| ------------------------------ | ------------------------------------------------ | ------- |
|
|
| Tab alert | While the dashboard is open | On |
|
|
| Browser title flash | Another tab in the same browser | On |
|
|
| Desktop notification | Another window on the same machine | Opt-in |
|
|
| Push notification | Anywhere, even with no tab open | Opt-in |
|
|
| Webhook (ntfy, Slack, Discord) | Anywhere, with no browser or subscription at all | Opt-in |
|
|
| Approvals Inbox | One queue across every session | Opt-in |
|
|
| Phone overview | Phone home screen, NEEDS YOU section | On |
|
|
| Away Digest | Afterwards, as a summary | Opt-in |
|
|
|
|
## Tab alerts
|
|
|
|
The tab itself changes state:
|
|
|
|
| State | Meaning |
|
|
| -------------------- | ---------------------------------------------------------- |
|
|
| Yellow, blinking | The agent is waiting for input from you. |
|
|
| Red, blinking | A question or permission prompt is blocking the session. |
|
|
|
|
These are a steady colour with a pulse layered on top, not a blink to transparent, so a tab
|
|
needing attention looks that way at every point in the cycle.
|
|
|
|
They survive a reload. The alert state is re-seeded from the server on page load, so
|
|
reloading the dashboard while a permission dialog is blocking a session does not leave you
|
|
with a normal-looking tab.
|
|
|
|
For Claude sessions, these come from Claude Code's hooks and are precise about *why* the
|
|
session stopped; DeepSeek Harness sessions report the same states themselves. For the other
|
|
CLIs there are no hooks, so you get the coarser output-based signal.
|
|
|
|
## Window title and OS notifications
|
|
|
|
The browser tab title is prefixed `codeman:<host>`, so several Codeman instances across
|
|
several machines stay distinguishable at a glance. Override the hostname with
|
|
`codeman web --title-hostname <name>`.
|
|
|
|
Desktop notifications use the same prefix. Enable them in **App Settings → Notifications**.
|
|
|
|
## Push notifications
|
|
|
|
Push reaches your phone with **no Codeman tab open at all**, which is the only option that
|
|
works while you are actually away.
|
|
|
|
Setup:
|
|
|
|
1. Open Codeman over **HTTPS**. Web push requires a secure context. Tailscale gives you real
|
|
HTTPS; `--https` gives you a self-signed certificate; plain HTTP over a LAN address will
|
|
not work.
|
|
2. **App Settings → Notifications → Subscribe**, and accept the browser prompt.
|
|
3. On **iOS**, add Codeman to your home screen first. Safari only delivers web push to
|
|
installed web apps, not to tabs.
|
|
|
|
Once subscribed, a blocking prompt reaches your phone even from a locked screen.
|
|
|
|
## Webhooks: ntfy, Slack, Discord
|
|
|
|
**Opt-in, off by default. One channel for the whole server.**
|
|
|
|
Push needs a browser that subscribed once. A webhook needs nothing on the client side: the
|
|
server itself posts each alert to an ntfy topic, a Slack or Discord incoming webhook, or any
|
|
URL as plain JSON. That makes it the option for a headless box nobody has opened in a browser,
|
|
and for a team channel.
|
|
|
|
It carries the same events as push: permission prompts, questions, idle sessions, session
|
|
errors, blocked respawns, a stopped crash loop and Ralph task completion. "Response complete"
|
|
is included only when **Which events** is set to **Everything**; the default, **Needs
|
|
attention**, skips it. A session that is watching its own work stays quiet here too.
|
|
|
|
Setup, in **App Settings → Notifications → Webhook**:
|
|
|
|
1. Pick the **Service**. ntfy gets a title, a priority and a tag per urgency; Slack and
|
|
Discord get a bold title line; **Generic JSON** posts `{ event, title, body, urgency,
|
|
sessionId, sessionName, host, at }`.
|
|
2. Paste the **Webhook URL** and turn on **Send alerts to a webhook**.
|
|
3. Press **Save**, either the group's own button or the main Settings Save, then **Send test**.
|
|
Send test saves anything you changed first, so it always tests what is on screen.
|
|
|
|
The status line under the group shows the last delivery: when it worked, or why it did not
|
|
(an HTTP status, a timeout, a refused connection).
|
|
|
|
Behaviour worth knowing:
|
|
|
|
- **The URL is a secret.** Anyone holding a Slack or Discord webhook URL can post as it, and
|
|
anyone who knows an ntfy topic can read it. Codeman keeps it in its own file,
|
|
`~/.codeman/webhook.json` (readable by its owner only), never in the shared settings, and
|
|
never shows it again: once saved, the box is empty and the hint shows only the scheme and
|
|
host. Paste a new URL to replace it, or press **Remove URL** to delete it from the server
|
|
(which also turns the channel off).
|
|
- **On public ntfy.sh, pick a long random topic.** Topics there are not private; the name is
|
|
the only thing keeping strangers out.
|
|
- **Local targets work.** A self-hosted ntfy on your LAN or on the same machine is fine.
|
|
Link-local and cloud-metadata addresses are refused, both when you save and when the
|
|
message is sent, and redirects are not followed.
|
|
- **Repeats are folded.** The same event for the same session within three seconds is sent
|
|
once, so a flapping prompt cannot flood a channel.
|
|
- **Multi-user mode: admins only, and it sees everything.** Only an admin can see or change
|
|
the webhook, and it receives every user's session events (session names, tool names, error
|
|
text). Point it somewhere every user would be comfortable with.
|
|
|
|
## The Approvals Inbox
|
|
|
|
**Opt-in, off by default. Claude sessions, plus DeepSeek Harness sessions, whose terminal
|
|
front door reports its prompts to Codeman.**
|
|
|
|
One queue of every prompt currently waiting on a human, across all your sessions, answerable
|
|
in place. When you have eight workers running, this is the difference between checking eight
|
|
tabs and checking one list.
|
|
|
|
Turn it on in **App Settings**. Surfaces:
|
|
|
|
- **A header bell** with a count, hidden entirely while the count is zero. Never shown on
|
|
phones.
|
|
- **A drawer** listing each waiting card.
|
|
- **NEEDS YOU strips** at the top of the phone overview home screen.
|
|
|
|
Each card shows the session, the case, and the captured prompt with its options. Answering
|
|
sends the keystroke into the session for you: a digit for a menu choice, Escape to decline,
|
|
or free text for an idle prompt.
|
|
|
|
Behaviour worth knowing:
|
|
|
|
- **One item per session.** A newer prompt supersedes the older one, because the older one
|
|
is no longer on screen.
|
|
- **Menu answers are validated against the live screen.** Codeman re-captures the pane before
|
|
sending, and refuses with a conflict if the dialog is no longer there. Otherwise your
|
|
keystroke would land in the composer as stray text.
|
|
- **Permission and question items clear only on definitive signals**: the turn ending, the
|
|
dialog completing, an answer, a supersede, the session exiting, or a 12 hour timeout. They
|
|
do not clear on a heuristic "looks busy again" signal, because that signal is wrong often
|
|
enough to lose a real prompt.
|
|
- **In memory only.** Restarting the server clears the queue; the prompts themselves are
|
|
still sitting in the sessions.
|
|
|
|
### Approve and Deny from the notification
|
|
|
|
With the inbox enabled, push notifications carry **Approve** and **Deny** buttons. Those are
|
|
handled by the service worker directly, so they work with no tab open: tap Approve on a
|
|
locked phone and the agent continues.
|
|
|
|
With the inbox off, the buttons are stripped from the notification payload entirely rather
|
|
than being shown and failing.
|
|
|
|
## When a session is watching its own work
|
|
|
|
An agent that starts a monitor, puts a shell in the background or hands a task to a cloud
|
|
session is told by its CLI to end the turn and wait to be notified. The pane then goes
|
|
quiet, and the CLI's idle notification arrives about a minute later — for a session that
|
|
wants nothing from you.
|
|
|
|
Codeman reads what the CLI prints about its own background work and treats that prompt
|
|
differently. It raises no tab alert, no desktop notification and no push, the session stays
|
|
out of NEEDS YOU on every surface, and the row wears a blue **watching** badge instead. Hover
|
|
it, or read it on a phone through your screen reader, and it says what is running: "1
|
|
monitor", "2 shells", "1 background terminal".
|
|
|
|
The prompt itself is not thrown away. It sits in the Approvals drawer as an ordinary card,
|
|
still answerable, with a line reading "quiet, watching 1 monitor" where a card you had
|
|
already looked at would say nothing. The next time that session goes quiet for an ordinary
|
|
reason, it alerts you exactly as before.
|
|
|
|
Two limits are worth knowing. A permission prompt or a question dialog still goes red
|
|
whatever else the agent started, because that one blocks it outright. A question asked in
|
|
plain prose is not a dialog, so an agent that starts a monitor and then writes "which branch
|
|
should I target?" is quiet along with the rest — check a watching session yourself if it has
|
|
been quiet longer than the work it is waiting for should take.
|
|
|
|
An agent waiting for your comments on an artifact it published never counts as watching.
|
|
Claude shows that as "1 Artifact comment monitor", but the agent hears nothing until you
|
|
comment, so the session alerts you like any other quiet session.
|
|
|
|
## The phone overview
|
|
|
|
On phones, tapping the "C" logo gives a session overview with **NEEDS YOU** first, then
|
|
current sessions, then past ones. Rows use the same language as the tab strip: a green dot
|
|
when fine, pulsing while working, yellow when waiting for input, red when a question is
|
|
pending.
|
|
|
|
Answer strips let you resolve a prompt straight from the home screen without opening the
|
|
session.
|
|
|
|
## The Away Digest
|
|
|
|
Retrospective rather than live: what happened while you were gone, aggregated from the
|
|
lifecycle log, run summaries, live sessions, token statistics, and recent subagents.
|
|
|
|
It is the morning-after view for an overnight run. Enable its header button in
|
|
**App Settings → Header & Panels**.
|
|
|
|
## Recommended setup for unattended runs
|
|
|
|
1. HTTPS access, ideally Tailscale. See [Remote Access](Remote-Access).
|
|
2. Push notifications subscribed, with Codeman installed to the home screen on iOS, or a
|
|
webhook to ntfy if no browser will ever be open.
|
|
3. Approvals Inbox on.
|
|
4. Auto-resume on usage limit on, for each session you leave running. See
|
|
[Keeping Agents Running](Keeping-Agents-Running).
|
|
|
|
That combination means a blocking question wakes your phone and can be answered in two taps
|
|
from the lock screen.
|
|
|
|
## Gotchas
|
|
|
|
- **No push over plain HTTP.** It is a browser requirement, not a Codeman one. A webhook
|
|
has no such requirement, since the server sends it.
|
|
- **iOS needs the home screen install.** A Safari tab will never receive push.
|
|
- **The bell is invisible at zero.** That is deliberate, not a broken setting.
|
|
- **Approvals need real signals.** They are built on hook events, which Claude emits and
|
|
DeepSeek Harness reports itself; the other CLIs do neither.
|
|
- **A stale menu answer is refused, not sent.** If you answer a card for a dialog that has
|
|
since gone away, Codeman declines rather than typing a digit into the composer.
|
|
|
|
## Read next
|
|
|
|
- [Keeping Agents Running](Keeping-Agents-Running) - what to configure before walking away.
|
|
- [Mobile Guide](Mobile-Guide) - the phone surfaces in full.
|
|
- [Settings Reference](Settings-Reference) - where each of these toggles lives.
|