Skip to content

Notifications ​

When you opt in, Qorin writes a small hook config into your project (e.g. .claude/settings.local.json for claude-code) telling the CLI to ping us when it needs your attention — typically when it's asking for permission to run a command, or when its turn is over and it's your move.

Two halves, and only one of them is paid. Seeing that a workspace needs you — the dot in the sidebar, the tab title, the row that moves to the top — is on every plan, Free included: a tool that knows something and doesn't tell you is not a cheaper tool, it's a worse one. What Pro+ adds is the notification that reaches you when you are not looking: the push on your phone, and the system notification from the browser.

The result: your browser tab pops a desktop Notification ("Claude is asking to run rm -rf node_modules — review →"), your phone gets a push, the workspace row in the sidebar pulses, and the page title gets a (1) badge so you spot it even when the tab is in the background.

One open request per workspace ​

This is the whole model, and it's worth reading before the rest of the page.

A workspace has at most one open attention request at a time. Not one per hook event: a single CLI turn fires several of them (a permission prompt, then the tool finishing, then the turn ending) and you still only have one thing to do. The second event of a turn doesn't send a second notification.

The request opens when the workspace lands on a state that is waiting for you:

StateWhat it means
awaiting_inputthe agent is asking for permission, or its turn ended and it's your move
idlethe CLI has gone quiet after a turn
failedthe process died — you'll want to know

The request closes on any of these, and the notification already delivered to your devices is withdrawn:

  • you open the workspace on any client — web, phone, or the Mac app
  • the CLI goes back to work (the workspace returns to running / spawning)
  • the process ends (completed / discarded)
  • you delete the workspace

Opening the workspace is the signal we lean on hardest, because it's the only one that doesn't depend on somebody else's contract: it travels over Qorin's own socket, we see it directly, and it works identically on every CLI — including one whose hooks tell us nothing at all.

One more case worth knowing: if you were waiting on a permission prompt and the process then dies, the old request is withdrawn before the new "workspace failed" one is sent. Two contradictory notifications about the same workspace, both from us, is how a notification badge stops being believed.

What "withdrawn" actually means, per platform ​

Withdrawal is best-effort and the platforms are not equal. The difference between "we implemented it" and "you'll see it" belongs in the docs, not in a support thread:

WhereWithdrawal
Browser tabCertain. The Notification object is ours; we close it.
AndroidCertain. A data message wakes the app's background service, which cancels by tag even with the app closed.
macOS appCertain while the app is running — the Mac app posts its banners from the workspace list it already holds, and pulls them the moment a workspace stops needing anybody.
iOSProbable, not guaranteed.

The iOS caveat, in full: withdrawal there needs a silent push, and Apple does not promise to deliver those. iOS throttles them on low battery, suspends them in Low Power Mode, and caps how many an app gets per day. When one doesn't arrive, the stale notification sits in the tray until you come back to the app — and opening the workspace closes the request for good.

That is a real limitation and not a reason to skip the feature: the worst case today (never withdrawn) was the only case before, and in between sits the large majority of the time when it works.

How to enable ​

Per-workspace, at spawn time, in the New workspace modal:

🔔 Notify me when the agent needs attention

The checkbox defaults ON, on every plan — it's what lights the attention dot, and that costs nothing to give you. On Free the dot is where it stops: no push to your phone, no browser notification.

You can flip it later from the workspace header (🔔 ↔ 🔕). The toggle takes effect on next spawn — changing it on a live workspace doesn't reach into the running CLI process.

What each CLI can actually tell us ​

The hooks are not equal either. This table is measured at the bench, not read off anybody's docs — an event that exists but never fires is worse than no event, because it looks like a feature:

CLI agentWhat its hooks report
claude-codeIt's asking for permission (Notification); its turn ended (Stop).
codexIt's asking for permission (PermissionRequest); its turn ended (Stop).
cursorIts turn ended (stop); it's working again (afterShellExecution, afterFileEdit, beforeSubmitPrompt).
geminiIt's asking for permission (Notification with a ToolPermission matcher); it's working again (AfterAgent).
opencodeIt's asking for permission (permission.updated); you answered it, and what you answered (permission.replied); its turn ended (session.idle).

Two honest notes on that table.

cursor has no "asking for permission" event. Its beforeShellExecution fires before cursor decides whether to ask you at all — under --force it never asks — so mapping it to "waiting for permission" would invent a state that isn't there. On cursor you get notified when the turn ends, which is the thing that genuinely happened.

opencode is the only one of the five that closes the loop. permission.replied says the user answered and what they answered. On claude-code, codex and gemini, granting or refusing a permission emits nothing at all — the first sign that you answered is the tool finishing, which can be 150 ms or twenty seconds later. That's why Qorin's own signals (you opened the workspace, you sent input, the process died) carry the closing side of the lifecycle for the other four. opencode isn't the straggler in this list; it's the one the other four are being measured against.

If a workspace stays quiet when you expect a nudge, run qorin-agent doctor on that host. The hook subcommand reachable line now names the CLIs this agent can wire notifications for:

✓ hook subcommand reachable   …/qorin-agent hook — notifications available
                              for: claude-code, codex, cursor, gemini, opencode

If your CLI is missing from that list, the agent on that machine is older than the support for it — update it and the workspace will pick it up on its next spawn. The list is read from the same code that writes the hook config, so it cannot drift into telling you something the binary can't do.

What gets written into your project ​

For claude-code, Qorin merges a hook block into <your-project>/.claude/settings.local.json:

json
{
  "hooks": {
    "Notification": [
      { "matcher": "", "hooks": [{
          "type": "command",
          "command": "qorin-agent hook --sock=… --cli=claude-code"
      }]}
    ],
    "Stop": [/* same shape */]
  }
}

That command is the agent binary itself; it posts to a per-workspace UNIX socket the agent listens on, and that socket forwards into Qorin's WebSocket and out to your devices. The socket is chowned to the workspace's OS user and chmod 0600, so another user on the same box can't talk to it.

Each CLI has its own file and its own schema:

CLI agentFile, relative to the workspace folder
claude-code.claude/settings.local.json
codex.codex/hooks.json
cursor.cursor/hooks.json (cursor also requires "version": 1)
gemini.gemini/settings.json
opencodea plugin file under .opencode/plugin/ — opencode has no hooks file, it has a plugin API, so Qorin subscribes to its event bus instead

Three rules govern those writes, and they exist because getting them wrong costs you settings you care about:

  • We merge, we don't replace. Only the hooks key is ours. Your permissions, model choices and MCP servers in the same file are left exactly as they were.
  • We put the original back. On cleanup the file is restored byte for byte — not re-serialised from what we parsed, which would lose key order and anything we don't model. A file we created is deleted; a file that was already there goes back to what it was.
  • A file we can't parse is left alone. If the config isn't JSON we understand, we don't touch it and the workspace runs without hooks. A missed notification is cheap; a clobbered config is not.

In worktree mode these files live in a throwaway worktree. In direct mode they live in your real project folder — which is exactly why the three rules above are not optional.

What gets removed when the workspace ends ​

When you terminate or delete the workspace, the agent removes its hook block (restoring whatever was there before) and unlinks the socket, so the CLI stops pinging us. Nothing of yours stays attached to Qorin after cleanup.

Permission prompt in the browser ​

The first time you enable notifications, your browser asks permission. If you deny by accident, re-enable from your browser's site settings on app.qorin.dev. The dashboard checks every load — if permission was revoked we silently fall back to the title-badge behaviour ((1) style), which doesn't need permission.

See also ​

Qorin is built by Benito Borriello.