-- NORMAL -- ~/projects/claude-tmux-notify.nvim.md project 02 / 06

claude-tmux-notify.nvim

When Claude Code is waiting for you in another tmux window, a toast drops into Neovim.

Stack
Lua · Neovim · tmux · ★ 3
Year
2026
Repository
github.com/Popoch39/claude-tmux-notify.nvim ↗
A

Anatomy

7 parts · only one counts
Fig. 1 Drop-flap annunciator board
  1. 01tmux-notify.shL9–59 ↗

    Write down the event

    Whenever Claude waits or finishes, appends a JSON line with its tmux window.

    CACHE_FILE

    9CACHE_FILE="${CLAUDE_TMUX_NOTIFY_FILE:-${XDG_CACHE_HOME:-$HOME/.cache}/claude-tmux-notify.jsonl}"

    10mkdir -p "$(dirname "$CACHE_FILE")" 2>/dev/null || true

    12input="$(cat)"

  2. 02uv.fs_statL149–162 ↗

    Start from the end

    On start-up, empties the file if it exceeds 1 MiB, then reads only new lines.

    M.config.max_cache_size

    149local path = resolve_cache_file()

    150pcall(vim.fn.writefile, {}, path, "a") -- ensure file exists (no-op append)

    155local st = uv.fs_stat(path)

  3. 03poll_intervalL164–170 ↗

    Watch the file

    Neovim checks every 1,000 ms whether the file has changed, without blocking the editor.

    uv.new_fs_poll()

    164local poll = uv.new_fs_poll()

    165state.handle = poll

    166poll:start(path, M.config.poll_interval, function(err)

  4. 04state.offsetL104–142 ↗

    Read the new lines

    Reads only the bytes added since the last read, and decodes each line.

    handle_lines()

    104local function handle_lines(chunk)

    105 for line in (chunk .. "\n"):gmatch("(.-)\n") do

    106 line = vim.trim(line)

  5. 05#{window_active}L64–74 ↗

    Check the active window

    Asks tmux whether Claude’s window is already on screen: if so, no alert.

    window_is_focused()

    65local function window_is_focused(window_id)

    66 if not (M.config.suppress_focused and window_id and window_id ~= "" and vim.env.TMUX) then

    67 return false

  6. 06warn · infoL86–102 ↗

    Compose the alert

    Titles the alert ‘Claude — 2:api-server’: warn while it waits, info once it is done.

    emit()

    86local function emit(event)

    87 if window_is_focused(event.window_id) then

    88 return

  7. 07vim.notifyL76–84 ↗

    Pick the notifier

    Goes through Snacks if it is installed, otherwise through Neovim’s own notifications.

    default_notify()

    76local function default_notify(message, level, opts)

    77 local Snacks = rawget(_G, "Snacks")

    78 if Snacks and Snacks.notifier then

1:nvim active · polling 1,000 ms
Note view

Click a window to make Claude wait, a dropped flap to plug into its jackTap a window to make Claude wait, a dropped flap to plug into its jack

Fig. 1 — Claude Code’s Notification or Stop hook appends a JSON line to the cache file; every 1,000 ms, Neovim reads whatever lies past its offset and raises a toast, unless Claude’s tmux window is already the active one.

Parts list

  1. 01 Tape punch The Claude Code hook: reads the event, asks tmux for its window and punches a five-field JSON line at the end of the tape. Always exits with 0. Code: scripts/tmux-notify.sh, lines 9–59 ↗
  2. 02 Reel and guillotine The cache file: at start-up it is truncated beyond 1 MiB, then reading starts from its end. Code: lua/claude-tmux-notify/init.lua, lines 149–162 ↗
  3. 03 Seconds escapement libuv’s fs_poll, which checks the file every 1,000 ms. Code: lua/claude-tmux-notify/init.lua, lines 164–170 ↗
  4. 04 Read head and buffer loop Reads the bytes past the offset, moves the offset on and decodes each JSON line. Code: lua/claude-tmux-notify/init.lua, lines 104–142 ↗
  5. 05 Corded plug Asks tmux whether the event’s window is active; sitting in that window’s jack, the plug holds its flap up. Code: lua/claude-tmux-notify/init.lua, lines 64–74 ↗
  6. 06 Flap The toast: titled Claude — <window>, with Claude’s message and a level set by the event. Code: lua/claude-tmux-notify/init.lua, lines 86–102 ↗
  7. 07 Trembler bell The notifier: Snacks if it is loaded, otherwise vim.notify at warn or info level. Code: lua/claude-tmux-notify/init.lua, lines 76–84 ↗