Skip to content

Latest commit

 

History

History
50 lines (29 loc) · 5.34 KB

File metadata and controls

50 lines (29 loc) · 5.34 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

claude-tmux-notifier is a Claude Code plugin (not a standalone app): it shows macOS notifications when Claude needs attention and clears them when you re-engage. Clicking a notification switches your tmux client back to the originating session+pane. The whole plugin is one bash script driven by hook declarations — there is no build step, no compiled artifact, and no test suite.

The files that matter

  • hooks/hooks.json — maps Claude Code hook events to scripts/tmux-notifier notify|clear. This is the control plane.
  • scripts/tmux-notifier — the single executable; all logic lives here.
  • .claude-plugin/plugin.json + .claude-plugin/marketplace.json — plugin and marketplace manifests. The repo is its own single-plugin marketplace (source: "./").

Architecture

Hook-driven and stateless. Each hook event runs the script with notify or clear; the script reads the hook's JSON payload from stdin and extracts session_id via jq. Every notification is grouped by claude-$session_id, so a session has at most one outstanding notification:

  • notifyterminal-notifier ... -group claude-$sid (a new notify replaces the prior one in the same group).
  • clearterminal-notifier -remove claude-$sid.

notify vs clear split (defined in hooks/hooks.json):

  • Create: Stop, StopFailure, Notification(permission_prompt|idle_prompt), Elicitation.
  • Clear: PreToolUse, PostToolUse, UserPromptSubmit, UserPromptExpansion, ElicitationResult, PermissionDenied, SessionStart(resume), SessionEnd.

Non-obvious timing rule — read before editing the hook set. Answering an in-conversation prompt does not fire UserPromptSubmit (that fires only for top-level typed prompts), and the permission_prompt notification fires asynchronously after PreToolUse has already run. That is why both PostToolUse (clears after the approved tool runs) and ElicitationResult (clears after an MCP elicitation answer) exist — without them, permission/elicitation notifications linger until the next tool call. When adding a new notify trigger, add the matching clear event too, or it will get stuck. PermissionDenied fires when the auto-mode classifier rejects a tool call — it does NOT fire when the user manually clicks Deny. Manual Deny still fires no hook; it relies on the next Stop notify replacing the notification.

Stop suppression (self-resuming turns). Stop fires at the end of every turn, including turns that armed background work (Monitor, ScheduleWakeup, Workflow, background Agent/SendMessage) — those sessions re-invoke themselves without user input, so notifying "waiting for input" is noise. On Stop, the script scans the just-ended turn in transcript_path (entries after the previous stop_hook_summary; the current turn's summary is written only after hooks run) and exits silently if such a tool call is found. Known gap: pending work armed in an earlier turn isn't detected (in practice self-resumed turns re-arm their monitors before stopping). Background Bash (run_in_background) is deliberately excluded — long-lived dev servers would suppress forever.

tmux detection is two-stage. Claude Code 2.1.x runs the session engine under a background daemon (bg-pty-host, parented to launchd), so hooks do not inherit TMUX/TMUX_PANE — the in-pane claude is just a client. The env vars are tried first (still work for direct runs), then the script falls back to matching the hook payload's cwd against pane_current_path across all panes. If neither finds a pane, the title degrades to Terminal with no click-to-focus.

Click-to-focus. Inside tmux the script builds an -execute command (tmux switch-client -c <client> -t <pane>) so clicking jumps back to the originating pane; the terminal app is raised via -activate <bundle id>, auto-detected from $__CFBundleIdentifier and overridable with CLAUDE_NOTIFIER_BUNDLE_ID. Outside tmux, only -activate is used. Clicking does not clear the notification (no hook fires from a click) — clearing is always hook-driven.

Runtime requirements

macOS + terminal-notifier (required; the script exits 0 as a no-op if it is absent) + jq. tmux is optional and only enables click-to-focus pane switching. The script reads CLAUDE_PLUGIN_ROOT for the notification icon path.

Validating changes (no test suite)

  • bash -n scripts/tmux-notifier — syntax check the script.
  • jq . hooks/hooks.json .claude-plugin/*.json — confirm the JSON parses.
  • End to end: echo '{"session_id":"test"}' | scripts/tmux-notifier notify 'hi', then echo '{"session_id":"test"}' | scripts/tmux-notifier clear.

Releasing changes (critical)

The version field is Claude Code's plugin cache key. Bump version in BOTH .claude-plugin/plugin.json and .claude-plugin/marketplace.json on every release — pushing commits without a bump leaves users (and /plugin update) on the cached copy. After install, hook changes need /reload-plugins or a session restart to take effect. For live local iteration, run claude --plugin-dir <repo path>, which reads from disk and bypasses the cache.

Conventions

Conventional-commit prefixes (feat:, fix:, docs:). Commits go directly to main.