This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
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.
hooks/hooks.json— maps Claude Code hook events toscripts/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: "./").
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:
notify→terminal-notifier ... -group claude-$sid(a new notify replaces the prior one in the same group).clear→terminal-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.
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.
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', thenecho '{"session_id":"test"}' | scripts/tmux-notifier clear.
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.
Conventional-commit prefixes (feat:, fix:, docs:). Commits go directly to main.