npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

claude-noti

v0.3.1

Published

Presence-aware notifications for Claude Code. Silent while you're watching the pane, a banner when you're not, a sound when you've walked away. macOS, Windows and Linux. Zero dependencies.

Readme

ClaudeNoti

Claude Code, but you can walk away.

npx claude-noti

Zero dependencies. macOS, Windows and Linux.


Why

Claude Code's value is that it works without you. But it stops — for permission prompts, for questions, for plan approvals — and you don't find out. So you either babysit the terminal, which defeats the point, or you come back twenty minutes later to discover it was blocked for nineteen of them.

The built-in terminal bell fires the same beep for everything and tells you nothing. With four sessions open across tabs and worktrees, a beep is not an answer to any question you actually have.

What it does

Three things, and the first is the one that matters.

1. It knows where you are

                 blocked      done
  ───────────────────────────────────────
  looking at it     —           —
  another window   ♪ banner    banner
  away             ♪ banner   ♪ banner
                   + phone     + phone

If you're looking at the pane, you already know, and nothing fires. Step into another window and you get a banner. Walk away from the desk and it makes noise, because across the room a silent banner is the same as nothing — and if you're properly gone, it reaches your phone.

The best notification is the one that never fires. Everything else here is in service of that row.

And if you walk away afterwards, it comes back. Presence is read at the moment the event fires, which quietly assumes the moment lasts. It doesn't: a prompt you were looking at goes correctly silent, then you go and make coffee, and nothing ever fires again. So an unanswered prompt is re-checked at two minutes and again at ten, escalating only if you've actually left. Anything you do in that session — answering, typing, the turn ending — cancels the whole chain. That's what makes it trustworthy rather than clever.

2. It says what, not just that

Not "Claude needs your attention":

ClaudeNoti · needs permission Bash · git push --force origin main

StobaeusVoice · done · wrote 4 files The echo report endpoint now validates before finalising.

Both are read from the transcript Claude Code is already writing. No API call, no latency, no cost.

Turn on the optional events and it also tells you before something alarming runs, and when a build breaks:

ClaudeNoti · about to run force-push · git push --force origin main

StobaeusVoice · command failed npm test · exit 1

3. It tells you which terminal

The title is the project, because with five sessions running that's the only thing that tells you where to go. With terminal-notifier installed, clicking the banner takes you there — right window, right tmux pane.

brew install terminal-notifier   # optional; enables click-to-focus

Install

npx claude-noti

One command does everything:

  • detects your OS and installs what it needs — terminal-notifier for clickable banners on macOS, and it names the packages on Linux rather than installing quietly into a platform it can't yet support
  • offers the ⌥Z panel and sets up tmux for it
  • runs doctor automatically on a first install, so you find out immediately rather than the first time something fails to appear
  • fires one real notification so you can see it work
  • backs up ~/.claude/settings.json, merges rather than overwrites, and leaves other tools' hooks alone

Takes effect in sessions started from then on. Run it again later and it upgrades in place, keeping your settings. --no-deps and --no-overlay skip those steps.

To undo it:

claude-noti uninstall            # keeps your settings
claude-noti uninstall --purge    # deletes those too

Commands

claude-noti                  status, sessions, and where you are
claude-noti board            every session on one screen, live
claude-noti phone            pair a phone
claude-noti doctor           check the wiring and explain what to fix
claude-noti test [event]     blocked | waiting | risky | failed | done
claude-noti last [n]         what it decided recently, and why
claude-noti focus [session]  jump to the terminal that needs you
claude-noti events           turn the optional events on and off
claude-noti segment          one line, for a statusline
claude-noti off | on         stop and resume
claude-noti mute 30m         resume automatically later
claude-noti config           print the effective settings

The panel — ⌥Z

Press ⌥Z and the board appears over your work. Press it again and it's gone, with everything underneath exactly as you left it.

  ClaudeNoti                       2 ●

  1 ● ClaudeNoti       blocked  0:42
      Bash · rm -rf dist
  2 ◐ StobaeusVoice    working  3:18

  ⌥Z to hide

It's a tmux popup, which means it's drawn over the pane rather than beside it. Claude Code isn't resized, redrawn, or detached — it just keeps running, and closing the panel leaves it exactly as it was. The primary thing stays primary. The panel also stays hidden entirely when no session is running.

claude-noti hotkey install     # binds ⌥Z in ~/.tmux.conf
claude-noti hotkey status      # is it wired, and will it reach this shell?
claude-noti hotkey remove

Run Claude Code inside tmux, since tmux is what receives the key:

alias claude='tmux new -A -s claude claude'

Why it isn't inside Claude Code. It can't be. Claude Code has no plugin API for panels, and ~/.claude/keybindings.json maps keys only to its own internal actions — there's no "run a command" to bind to. Verified against 2.1.220. So the key has to be caught before Claude Code sees it, and tmux already sits in that position, reading every keystroke on the way through.

The alternative was a global hotkey daemon like Hammerspoon. That was built first and then removed: it meant a GUI app resident all day, an Accessibility grant, and a different tool on every OS. tmux needs none of those and behaves identically on macOS, Linux and Windows.

Why ⌥Z and not ⌘⌥Z: macOS never delivers ⌘ combinations to a terminal program — the terminal swallows them. Option comes through as an escape prefix. ⌘ is only reachable via a background helper; ⌥ is reachable without one, which is the entire point.

One key does both directions: tmux opens the popup, and once open the keys go to the board inside, so the same ⌥Z arrives as an escape sequence the board reads as quit.

The full board

claude-noti board is the same board full-screen in its own pane, with keys that work. It redraws when something changes rather than on a timer, so it costs nothing to leave open in a split.

  ClaudeNoti                       2 ●

› 1 ● ClaudeNoti       blocked  0:42
      Bash · rm -rf dist
  2 ◐ StobaeusVoice    working  3:18
  3 ✓ MarkR            done     12:00

  1-9 jump   m mute   ? keys   q quit

19 jumps to that session's terminal, m mutes a noisy one for half an hour, q quits. It degrades down to 24 columns, dropping the state word before it ever drops a project name.

For the same thing over your work instead of beside it, use ⌥Z above.

In your statusline

claude-noti segment prints one short line and exits — ● 2 blocked, or nothing at all when nothing needs you. MarkR has no plugin API for third-party segments, so this is the seam: call it from any statusline that runs a command.

Run claude-noti test blocked in the terminal you're looking at and it will correctly refuse to notify you. That's not a bug to work around — it's the product demonstrating itself. Add --force to hear it anyway.

When something seems missing, claude-noti last shows the decision and the reason, including the silences:

  12:51:39  blocked  silent · here
  12:47:02  done     banner+sound       Statusline renders git dirty state.

Your phone

claude-noti phone

Generates a random ntfy topic, copies the URL for you, and tells you the two steps on the phone. No account, no signup, iOS and Android.

Pushes are terse by default, and that is deliberate.

An ntfy topic is a URL. Anyone who learns it can read everything ever posted to it, and there's no undo. Notification bodies carry the command Claude wants to run and the last thing it said — repository names, file paths, and occasionally a secret sitting in an argv.

So over ntfy you get ClaudeNoti · needs permission and nothing more: the project, and what it needs. Set "mobile": { "detail": "full" } to opt in knowingly.

The topic name is the only credential in the system, which is why it's generated from crypto.randomBytes and never something you choose.

If you'd rather nothing left your own account at all:

claude-noti phone imessage +15550100

That texts you through Messages — your Mac to your own number, no third party involved, and it reaches an Apple Watch. Full detail is on by default there for exactly that reason.

Push only fires when you're away, and only for events that need an answer — blocked, waiting, risky, failed. done is deliberately not on that list: a phone that buzzes on every completed turn is a phone you silence within a day, and then the ones that mattered don't reach you either. Add "done" to mobile.events if you want it.

Optional events

Two features are off by default because they ride PreToolUse and PostToolUse, which fire on every tool call:

claude-noti events risky on    # force-push, rm -rf, prod, migrations, secrets
claude-noti events failed on   # a build or test command exited non-zero

Measured cost on an M-series Mac: about 27 ms per tool call, essentially all of it Node startup. Use one command rather than editing the config by hand — the flag and the hook have to move together, and claude-noti events … on does both. doctor reports the drift if they ever come apart.

risky is the one event that speaks even when you're looking at the pane. The presence rule assumes that if you're watching, you already know — true of a permission prompt, which fills the screen, and false of the exact flags on a command scrolling past. Patterns are narrow and named, and any one of them can be switched off:

{ "risky": { "ignore": ["production"] } }

Configuration

~/.claude/noti.json, or .claude/noti.json inside a project to override it there. Anything you leave out keeps its default — there is no file to generate and no defaults frozen into your config at install time.

{
  "presence": {
    "enabled": true,        // false makes this an ordinary, always-firing notifier
    "awaySeconds": 300      // no input for this long means you left the desk
  },

  "levels": { "blocked": "loud", "risky": "loud", "failed": "medium", "done": "soft" },

  // presence x level -> channels. This table is the whole product.
  "routes": {
    "here":   { "loud": [],                              "soft": [] },
    "nearby": { "loud": ["banner", "sound"],             "soft": ["banner"] },
    "away":   { "loud": ["banner", "sound", "push"],     "soft": ["banner", "sound", "push"] }
  },

  // What happens when nobody answers. `after` is seconds from the event.
  "escalation": {
    "enabled": true,
    "steps": [
      { "after": 120, "presence": ["nearby", "away"], "level": "loud" },
      { "after": 600, "presence": ["away"], "channels": ["push"] }
    ]
  },

  "mobile": {
    "backend": "off",          // ntfy | imessage | both | off
    "detail": "terse",         // terse | full — see the warning above
    "events": ["blocked", "waiting", "risky", "failed"]
  },

  "events": { "risky": false, "failed": false },  // the every-tool-call hooks

  "sounds": { "loud": "Ping", "soft": "Pop" },   // from /System/Library/Sounds
  "banner": { "clickToFocus": true }
}

Arrays are replaced wholesale, never merged — "loud": ["banner"] means exactly that. So to add a risky pattern, copy the list and append to it.

A project that should never interrupt you:

// ~/scratch/.claude/noti.json
{ "enabled": false }

How presence works

Three probes, none of which needs a permission prompt:

| | macOS | Windows | Linux | |---|---|---|---| | idle | ioreg HIDIdleTime | GetLastInputInfo | xprintidle | | frontmost app | lsappinfo — faster than System Events, and no Accessibility grant | GetForegroundWindow | xdotool | | active pane | tmux, when the session lives in one | — | tmux |

On Windows both facts come back from a single PowerShell call, because the cost there is the interpreter starting, and paying that twice to ask two questions would waste most of a second on every notification. It runs in the detached worker, never in the hook.

Rather than mapping TERM_PROGRAM onto application names — a table that is wrong the day someone ships a new terminal, and ambiguous today, since VS Code, Cursor and Windsurf all report vscode — ClaudeNoti records whichever app was frontmost at the moment you last typed a prompt. At that instant you were, by definition, looking at your terminal. It calibrates itself against whatever you use, and re-calibrates on every prompt.

A probe that fails falls back to nearby, never to here. A broken detector degrades into an ordinary notifier — mildly annoying, never useless. Silence is the one unrecoverable failure: you never find out it happened. This is why Wayland, where neither fact is obtainable, gets a notifier that always fires rather than one that quietly stops.

How it works

Six hooks by default — SessionStart, UserPromptSubmit, PermissionRequest, Notification, Stop, SessionEnd — plus PreToolUse and PostToolUse only if you turn the optional events on. No daemon.

Claude Code waits on hooks, so the hook does almost nothing: parse the payload, write a small session file, hand the job to a detached worker, exit. Presence probes, transcript reads and banners all happen in that worker, which Claude Code has no handle on and never waits for. A hook takes about 27 ms, nearly all of it Node startup.

Escalation needs no daemon either. A blocked event spawns one detached timer that sleeps, wakes, re-reads the session file, and acts only if the world still looks the way it did. Cancellation is a data comparison rather than a signal: every session carries an escalationId, stamped fresh whenever it enters a blocked state, and a timer whose id no longer matches simply exits. Nothing has to find it, signal it, or clean it up — including a timer whose machine slept through its own deadline.

Session state is a directory of small JSON files under ~/.claude/noti/, which is what makes the multi-session board a directory read rather than a service to supervise.

Platforms

Notifications, sounds, presence and phone push work on all three. What differs is the two features that need to reach into the window manager.

| | macOS | Windows | Linux | |---|---|---|---| | banner | terminal-notifier, else osascript | toast via PowerShell | notify-send | | sound | afplay | .NET system sounds | libcanberra / paplay | | presence | ioreg + lsappinfo | GetLastInputInfo + GetForegroundWindow | xprintidle + xdotool, X11 only | | phone push | ✓ | ✓ | ✓ | | click to focus | ✓ | — | — | | ⌥Z panel | ✓ | — (no tmux build) | ✓ |

Two honest caveats. Under Wayland there is no presence detection at all — the protocol deliberately refuses to tell an unprivileged client what you are looking at — so ClaudeNoti degrades to an ordinary notifier that fires every time. And on Windows the ⌥Z panel does not exist, because it is a tmux popup; claude-noti board in a split pane is the same board.

claude-noti doctor reports which of these your machine actually has, rather than assuming.

Not yet

  • Replying from your phone — approving a permission prompt from the notification. It's the obvious next prize and it's deliberately deferred: it needs something listening on the Mac to receive the answer and inject it into a live session, which is exactly the daemon this design avoids. That earns its own design round, not a corner of this one.
  • A QR code in the terminal for pairing. Hand-written QR encoders are fiddly in a way that fails silently — an attempt here was checked against a reference encoder and was wrong in about a third of its modules, which would have shipped as a code that looks convincing and doesn't scan. Fixing it properly meant a dependency, and zero dependencies is the rule. claude-noti phone copies the URL and offers to open it, where ntfy renders its own.

Also deliberately absent, and not planned: Electron, a cloud account, remote control of Claude, and LLM-written summaries by default.

License

MIT