Skip to content
kiril6Public

About

Procedural focus music that follows your AI coding agent: it plays while the agent works, pauses when it needs you, and chimes when it's done. Zero dependencies, no audio files.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

🎧 VibeAudio

VibeAudio — Focus music while your AI codes

test npm license node downloads/month M8ven Score

VibeAudio - Focus music while your AI codes. | Product Hunt

Procedural focus music while your AI coding tools think. Every project gets its own arrangement. Zero dependencies, zero audio files. Works with Claude Code, Codex, Cursor, Grok, Gemini CLI, Copilot CLI, Qwen Code, Windsurf, Antigravity, Aider — and any terminal command.

Install · Agent hooks · Genres · Flags · Troubleshooting · Uninstall

🔊 Listen to every genre → — hear all 10 genres, the tier escalation, and the three chimes, rendered from the real synth.

In a hurry? npm i -g vibeaudio, then vibe --install-hooks. Your next prompt has music.

Only Claude Code or Codex? Skip npm. Inside a Claude Code session: /plugin marketplace add kiril6/vibeaudio, then /plugin install vibeaudio@vibeaudio. For Codex: codex plugin marketplace add kiril6/vibeaudio, then codex plugin add vibeaudio@vibeaudio (details).


⚡ Why

AI coding agents take 15–45 seconds to reason, read files and write code. Staring at a blank cursor feels slow; tabbing away means checking back to see if it's done. VibeAudio fills that gap with music while the agent thinks, and a chime when your output is ready to read.

It is built as calm technology, in the sense of Mark Weiser and John Seely Brown's Designing Calm Technology (Xerox PARC, 1995): information that stays at the edge of your attention and only comes to the front when it matters. You stop noticing the music, but you notice when it changes.

Principle In VibeAudio
Inform without demanding attention The music sits in the background. What you notice is it stopping.
Come to the front only when it matters Chimes for done, failed and "needs you" only. The music doesn't react to every tool call unless you ask it to (reactive mode).
Make the background informative Layers build as a turn runs long, and a heartbeat joins when an agent keeps failing. That fires in under 1% of turns, by design.
Calm, not alarming "Needs you" is a soft rising interval, not an alarm. A mute ends on its own after an hour.

How it behaves:

  • 🧮 Pure synthesis, zero MP3s. Every note, chord and pad is generated in code — no audio assets, no npm dependencies, no install scripts, no node-gyp.
  • 🍃 Light on battery. Each loop is rendered once and cached. While music plays, your OS's own audio player does the work. With hooks, nothing of VibeAudio's keeps running between turns.
  • 🎼 A different arrangement per project. Your working directory seeds the progression, bass line and melody, so each repo has its own sound and keeps it.
  • 📈 Escalating layers. Tier 1 (0–15s) gentle intro → Tier 2 (15–45s) main groove → Tier 3 (45s+) deep focus. You can hear how deep into the task the agent is.
  • 🔁 A phrase, not a loop. Each piece is three bars that rotate through your project's own progressions, so a long turn moves through a ~21-second phrase instead of replaying one 7-second bar for half an hour.
  • 🎛️ Music that follows the work (reactive mode, opt-in). Calm while the agent reads, fuller while it edits, busiest when it runs commands or hands work to sub-agents — you can hear what it's doing, not just for how long.
  • 🌅 It fades, it doesn't cut. On macOS the music eases in and out instead of starting and stopping mid-note, and vibe --volume reaches music that is already playing within a second.
  • 🔔 Outcome-aware chimes. Ascending on success, a soft descending minor chord on failure, and silence on Ctrl+C — an abort is never reported as done.
  • 💓 A heartbeat when an agent looks stuck. If 4 of a session's last 8 tool calls fail — the test-edit-test loop that goes nowhere — a soft pulse on the music's tonic joins the music until things start passing again. It's rare by design: on 4,497 real turns it fired in under 1%.
  • ✋ A "your turn" chime. When Claude Code stops to ask permission (or an MCP server asks for input), the music pauses and a rising two-note chime asks for you; it picks back up once you've answered.
  • 🔕 Signals without the music. vibe --music off keeps the done, failed and "needs you" chimes, banners and the state stream, and drops the music. For people who bring their own, or share an office. (details)
  • 🔌 Universal drop-in. Hooks for Claude Code, Codex, Cursor, Grok, Gemini CLI, Copilot CLI, Qwen Code, Windsurf and Antigravity; MCP for Claude Desktop; the wrapper (vibe <command>) for anything else.
And the quieter four — silence on fast commands, several sessions at once, exit codes, the HUD
  • 🛡️ A grace window. Fast commands stay 100% silent — music starts only past 1.5s (--grace).
  • 🪟 Several sessions, one soundtrack. Run as many terminals of the same agent as you like: the music plays while any of them is working, and each finishes with its own chime. One session ending, pausing for a permission dialog or being interrupted never cuts off another that's still going.
  • 🧮 Honest exit codes. Your command's status passes straight through (130 on Ctrl+C), so vibe claude && next-step behaves exactly as it would without the wrapper.
  • 🌊 Terminal title HUD. A live ASCII wave and elapsed timer in the window title ([ ♫ ▃▅▆▇█▇▆▃ lofi (0:24) ]), where it can't corrupt a full-screen TUI.

🎼 Every project gets its own arrangement

VibeAudio seeds the composition from your project directory. The repo you're in picks the chord progression, the bass line, the melodic contour, and where the ornaments land — so ~/work/api and ~/side/game genuinely sound different, while each one sounds the same every time you come back to it.

Pin or explore arrangements when you want to:

vibe --seed 42 --preview jazz    # audition one specific arrangement
export VIBE_SEED=7               # pin the same sound everywhere
Why the same sound every time, rather than something new — and what "procedural" actually means

That's a deliberate choice. Focus music has one job: be ignorable. Music that reinvents itself every run keeps pulling your ear back, which is the opposite of what you want while reading an agent's output. Familiar-per-project gives you variety across contexts and predictability within one — by your third session in a repo, its loop has faded into the furniture.

How "procedural" actually works — worth being precise, since it shapes what you'll hear. Each genre is synthesized from code (oscillators, chord tables, envelopes rendered to PCM), not shipped as audio files. The seed selects among hand-written, human-checked variants — three progressions per genre, each diatonic to that genre's key, plus seeded ornament placement — so it never invents harmony and can't wander out of key. The chosen arrangement is rendered once per project, cached under ~/.vibeaudio/cache/, and looped. Within a session you're hearing a 6–8 second bar repeat that gains layers as you cross the tier boundaries.


🎧 Send someone your repo's sound

vibe --render            # -> vibeaudio-lofi.wav, ~53s
vibe --genre jazz --render our-api.wav

A whole session in one file: every bar of the phrase at tier 1, then tier 2, then tier 3, and the success chime at the end — rendered from your project's own seed, at full scale so your player's volume control is the only one in the way. Plain WAV, so nothing has to be installed to play it.

📋 Requirements

  • Node.js ≥ 18
  • An audio player. VibeAudio shells out to whatever your OS provides:
Platform Player used Volume control Notes
macOS afplay ✅ Ships with the system — nothing to install.
Linux paplay, ffplay, or aplay ✅ Install pulseaudio-utils, ffmpeg, or alsa-utils.
Windows PowerShell SoundPlayer ✅ Nothing to install.

aplay and PowerShell's SoundPlayer take no volume argument, so --volume is applied by rendering the loop pre-attenuated instead — same result, one cached file per volume you use.

If no player is found, VibeAudio prints a one-line notice and runs your command silently — it never blocks the tool you actually wanted to run.


🚀 Install

One line. No clone, no build step, no dependencies to resolve:

npm i -g vibeaudio

That puts vibe and vibeaudio on your PATH. Re-run the same command to update, or see Uninstall to remove it cleanly.

Want what's on main? Fixes land here before they reach the registry, and installing from GitHub gets them straight away — no clone, no build:

npm i -g github:kiril6/vibeaudio

Same command to update it again. Switch back to the released version any time with npm i -g vibeaudio.

Staying up to date. When a newer version is out, vibe --status, vibe --help and the menu say so in one line. They're the only places it's shown — never in your agent's hooks, and never over a command you wrapped. What changed is on the releases page.

What the update check sends, and how to turn it off

Once a day at most, a background process asks the public npm registry for the latest version number — the same request npm outdated makes. It sends nothing about you: no ID, no settings, no usage. It's the only network request VibeAudio ever makes, and vibe never waits for it; the answer is shown the next time. It's skipped in CI, under npx, and whenever VIBE_NO_UPDATE_CHECK=1 or NO_UPDATE_NOTIFIER=1 is set.

Then pick how it runs

Using Claude Code, Codex, Cursor or Grok interactively? Install the hooks — this is the mode that actually tracks thinking:

vibe --install-hooks

Want the music to follow what the agent is doing — calm while it reads, busiest while it runs commands? Add --reactive:

vibe --reactive --install-hooks

Prefer a guided setup? Run vibe with nothing after it: a menu asks for your agent, genre and volume (press p to hear a genre first) and installs the hooks for you. The menu's last entry, Alerts & extras, flips the saved toggles (banners, spoken announcements, vibe --announce, music, a one-hour mute) without a flag to remember. Installing hooks doesn't start the agent: open it whenever you like, and the music plays from your first prompt. ("Just launch it" and "This session only" do start it.)

Now run your agent normally, with no prefix. Music starts when you submit a prompt and stops with a chime when the agent finishes. Details below.

Running one-shot commands? Wrap them:

vibe npm test
vibe claude -p "explain this repo"

Lost? vibe --help lists every flag, and vibe --status reads your live setup back to you — what's installed, what's playing, and which agents it found. Its first line says why the music is silent or playing right now (silent: muted for 42 more min (vibe --unmute), playing: lofi @ 40%, 2 sessions working (api, web)). If something seems broken, vibe --doctor checks it and prints the fix for each problem.

Which one you want: the wrapper plays music for as long as the wrapped process lives. That's exactly right for a command that exits when its work is done — and wrong for an interactive REPL like claude, where the process stays alive while you read and type, so the music never stops. Hooks know when the agent is actually thinking; the wrapper can only time the process.

Try it without installing

npx vibeaudio --preview jazz   # hear a loop right now
npx vibeaudio sleep 8          # hear the wrapper: music, then the done chime

Heard nothing? Music only starts once the wrapped command has run longer than the 1.5s grace window — that's the point, so quick commands stay silent. sleep 8 is the reliable demo; something like npm test is silent if the tests finish fast or the command errors out immediately.

npx runs from a temporary cache that npm eventually deletes, so --install-hooks refuses to run this way — it would write a path into your agent's hook config that later vanishes. Install globally first.

Interactive Launcher Menu

Run with no command to get a menu for picking your AI, vibe, and volume:

vibe
🎧 VibeAudio — Interactive AI Launcher

Which AI companion would you like to launch?
❯ 1. Claude Code          [✓ installed]
  2. Gemini CLI           [✓ installed]
  3. Codex CLI            [✓ installed]
  4. GitHub Copilot CLI   [✓ installed]
  5. Grok CLI             (not found in PATH)
  6. Cursor CLI           (not found in PATH)
  7. Aider                (not found in PATH)
  8. Ollama (Llama 3)     (not found in PATH)
  9. Custom command...

Choose your sound vibe:
❯ 1. ☕ Lo-Fi Focus        — Warm Rhodes electric piano & Kalimba drops
  2. 🌌 Chill Synthwave    — '80s analog chorused pads & pulsing bass
  3. 🕹️ Cozy 8-Bit         — Filtered retro chiptune arpeggios
  4. ⚡ Melodic Electronic  — Downtempo resonant plucks & tech pulse
  5. 🎷 Midnight Jazz      — ii-V-I piano chords & walking upright bass
  6. 🎋 Zen Ambient        — Meditative singing bowls & floating celestial pads
  7. 🎹 Sparse Piano       — Single struck notes & long silences — Satie-ish
  8. 🌫️ Deep Drone         — Held tone & filtered noise — no melody at all
  9. 🎲 Shuffle / Random   — Picks a surprise vibe each time

Choose your volume level:
❯ 1. ☕ Normal (40%)       — Balanced focus background [Default]
  2. 🤫 Quiet (25%)        — Discreet focus / open office
  3. 🌙 Whisper (15%)      — Ultra-gentle / headphones / late night
  4. 📢 Loud (75%)         — Audible across the room

Press p to hear the highlighted genre — auditioning is just a keypress, and moving on replaces it.

Installed tools sort to the top, and the list is a shortcut rather than a compatibility list: vibe wraps any command at all, and "Custom command..." takes one you type.

Picking a hook-capable agent asks two more questions

Pick Claude Code or Codex and it asks how the music should run — hooks or just this session — and the hooks branch then offers reactive mode too:

How should the music run?
❯ 1. Agent hooks          — Music follows the agent's thinking. Set once, no wrapper needed
  2. This session only    — Music plays while the process lives - fine for one-shot commands

Should the music react to what the agent is doing?
❯ 1. Steady (recommended) — Intensity follows elapsed time, and stays ignorable
  2. Reactive             — Intensity follows the tool in use - noticeable, by design

Choosing hooks installs them for the tool you picked, then launches it — the same thing vibe --genre <g> --volume <n> --reactive --install-hooks does, without memorising flags. Once they're installed the first question becomes Just launch it / Reconfigure the hooks, and launching skips the genre and volume prompts, since your saved default already answers those (vibe --genre zen changes it any time).

Adding an entry to the launcher is one line in src/interactive.js.


🪝 Agent Hooks (no wrapper needed)

--install-hooks supports Claude Code, Codex, Cursor, Grok, Gemini CLI, GitHub Copilot CLI, Qwen Code, Windsurf and Antigravity. Everything else uses the wrapper or MCP instead.

Wrapping (vibe claude) infers "the AI is thinking" from how long the process runs. Hooks know for certain — so music starts the moment you submit a prompt and stops the moment the agent finishes, with no grace-window guessing and no aliases.

Once hooks are installed they take over: running vibe claude (or vibe codex) anyway plays no music of its own and says so, rather than layering a session-long loop on top of the hooks' per-prompt one.

vibe --install-hooks                             # every agent found on this machine
vibe --install-hooks --tools codex               # or just one of them
vibe --genre jazz --volume 25 --install-hooks    # install, and save these as your default
vibe --install-hooks --dry-run                   # show what would change, write nothing

It auto-detects. With no --tools, VibeAudio wires up each supported agent it finds on your machine — one counts as present when its config directory exists or its CLI is on your PATH. --tools claude,codex,cursor,grok,gemini,copilot,qwen,windsurf,antigravity overrides that. Add --dry-run to see, per event, what would be added or changed in each file before anything is written.

That's it — run your agent normally, with no vibe prefix.

Which file and which events, per agent — nine dialects, all written for you

Each tool spells its events its own way, and VibeAudio writes whichever dialect the file expects:

File Music starts Music stops + chime Reactive (opt-in)
Claude Code ~/.claude/settings.json (or $CLAUDE_CONFIG_DIR) UserPromptSubmit Stop PreToolUse
Codex ~/.codex/hooks.json (or $CODEX_HOME) UserPromptSubmit Stop PreToolUse
Cursor ~/.cursor/hooks.json beforeSubmitPrompt stop preToolUse
Grok ~/.grok/hooks/vibeaudio.json (or $GROK_HOME) UserPromptSubmit Stop PreToolUse
Gemini CLI ~/.gemini/settings.json (or $GEMINI_CLI_HOME/.gemini) BeforeAgent AfterAgent BeforeTool
Copilot CLI ~/.copilot/hooks/vibeaudio.json (or $COPILOT_HOME) UserPromptSubmit Stop PreToolUse
Qwen Code ~/.qwen/settings.json (or $QWEN_HOME) UserPromptSubmit Stop PreToolUse
Windsurf ~/.codeium/windsurf/hooks.json pre_user_prompt post_cascade_response pre_run_command
Antigravity ~/.gemini/config/hooks.json, under its own vibeaudio name PreInvocation (the first of each prompt) Stop — never (why)

Where an agent reports more than start and stop, VibeAudio listens for that too — and only where the event was confirmed against the tool itself:

Music pauses + "your turn" chime Music resumes Failure chime (API error) Session closes mid-turn (silent)
Claude Code PermissionRequest, Elicitation PostToolUse, PostToolUseFailure, ElicitationResult StopFailure SessionEnd
Codex PermissionRequest PostToolUse — SessionEnd, and Interrupt (Esc)
Gemini CLI Notification (tool permission) AfterTool — SessionEnd
Copilot CLI Notification (permission prompt) PostToolUse, PostToolUseFailure — SessionEnd
Qwen Code PermissionRequest PostToolUse, PostToolUseFailure StopFailure SessionEnd
Antigravity — — Stop ending in an error or the step limit —
Cursor, Grok, Windsurf — — — —
Seven things that differ per agent — only the first one needs anything from you
Five agents tell you when they're waiting on you When a permission dialog opens the music stops rather than sounding busy while the agent is stuck on you, and it resumes once the thing you answered has run. Claude Code covers the terminal, desktop app and IDEs alike, plus MCP servers asking for input. Copilot CLI's own PermissionRequest fires before every permission check — dialog or not — so VibeAudio listens for its permission-prompt notification instead. Cursor and Grok have no such event that's been verified, so they keep playing through a prompt.
Claude Code turns that never reach Stop still end the music An API error or rate limit ends the turn with StopFailure instead, which plays the failure chime. Interrupting (Esc, or the stop button in the desktop app) fires no hook at all, so the background player watches the session transcript for Claude Code's interrupt entry and stops silently within half a second — whether the agent was writing or running a tool. A prompt that another hook blocks (a token-saving proxy, say) never reaches Stop either, so the same watcher ends the music on Claude Code's "prompt blocked" entry — or never starts it, when the blocker was quicker than we were. Closing the session mid-turn stops it too — but only if that session started the music, so closing an idle terminal never silences another one.
Codex asks you to trust the hook once Codex keeps a per-hook trust hash in ~/.codex/config.toml and won't run a hook it hasn't been told to trust, so the install isn't live until you approve each one the first time it fires.
Cursor and Antigravity say how a turn ended Cursor's stop event reports whether the turn completed, aborted or errored, and Antigravity's gives a termination reason, so an error plays the failure chime. Claude Code and Qwen Code send a separate StopFailure for an API error. The others send no verdict, so a turn there always ends on the success chime — VibeAudio won't invent a failure the agent never claimed.
Antigravity never gets a pre-tool hook Its pre-tool hook has to approve or deny every tool call — one that answers nothing blocks the tool — so VibeAudio installs none, and --reactive does nothing there. It has no prompt event either: the start event fires before every model call, and VibeAudio starts the music on the first one of each prompt only. Its hooks live in ~/.gemini/config/hooks.json under a name of their own, so your other named hooks there are never touched. Verified with the agy CLI; the desktop app reads the same file by Antigravity's own docs but hasn't been checked live.
Upgraded, and a new feature isn't there? Hooks are written into your agent's config once, at install time, and an upgrade doesn't touch them. When a new version listens for more events (0.13.1 added Codex's Interrupt and SessionEnd), vibe --doctor and vibe --status name what's missing, and vibe --install-hooks adds it.
Moved your config folder? VibeAudio follows CLAUDE_CONFIG_DIR, CODEX_HOME, COPILOT_HOME, GEMINI_CLI_HOME, QWEN_HOME and GROK_HOME the way each agent does, so hooks (and /vibe) go where your agent actually reads them. Cursor and Windsurf have no such variable that we could confirm. Set the variable in the shell you run vibe --install-hooks from.
Grok and Copilot CLI get a file of their own Each reads every *.json in its hooks/ directory, so VibeAudio writes vibeaudio.json rather than merging into anyone else's — which makes uninstalling it a delete, and leaves no backup file behind. Copilot's honours COPILOT_HOME.

Several sessions at once

Every session of an agent is tracked separately, by the session id in its hook payload, so two terminals (or a terminal and the desktop app) share one soundtrack instead of fighting over it:

  • The music plays while any session is working. A second prompt doesn't restart it, and it stops only when the last working session finishes.

  • Every session gets its own chime. A quick question that finishes while another agent is still busy chimes "done" and the music carries on underneath.

  • Which one? With three terminals going, a chime says something finished and leaves you alt-tabbing to find out what. vibe --notify adds a desktop banner (api: finished, web: needs you (Bash)) and vibe --speak adds a spoken line. Both are off by default. Banner details below.

  • Say it out loud. vibe --speak adds a spoken line after the chime ("api: done.") when two or more sessions are going. Details below.

The rest of how sessions share one stream
  • More sessions, more music. Two sessions working at once plays at least tier 2 and three or more plays tier 3 — the same piece with more layers, arriving at the next loop boundary and easing off as sessions finish. It only ever raises the tier, so it works alongside time escalation and reactive mode.
  • Dialogs and Esc are per session. A permission dialog in one terminal plays the "your turn" chime but only pauses the music once every session is waiting; Esc ends just that session's turn.
  • Crashed agents can't hold it hostage. A session that goes 15 minutes without any hook (no tool call, no Stop) is taken for a crashed agent and dropped. A long turn that keeps working keeps its music.

Sessions with no id in their payload share a single slot, so they behave as one. VibeAudio keeps one stream: per-session genres or several streams mixed together aren't supported.

No restart needed, even mid-session — for Claude Code, Codex, Cursor and Grok. Each re-reads its hook file every time a hook fires, so changes land on your next prompt. A daemon already playing keeps its old settings until that prompt replaces it — at most the tail of one turn. Whether an open Gemini CLI, Copilot CLI, Qwen Code, Windsurf or Antigravity session does the same hasn't been checked, so start a new session there to be sure.

The Claude Code desktop app is covered too, not just the terminal — both read the same ~/.claude/settings.json (or $CLAUDE_CONFIG_DIR/settings.json). (The separate Claude Desktop chat app is a different product with no hooks; that one needs MCP.)

One player is shared. Prompt two agents at once and the last prompt owns the music. One person, one set of speakers — deliberate, not a limitation being worked around.

🔔 Desktop banners: vibe --notify

vibe --notify       # on
vibe --no-notify    # off again (the default)

A banner names the project when a turn finishes, fails, needs you (a permission dialog or a question) or looks stuck: api: finished, web: needs you (Bash). It is saved, so it applies from your next prompt with nothing to reinstall. vibe --status shows it on the notify line, the Alerts & extras screen in the vibe menu toggles it, and VIBE_NOTIFY=1 turns it on for one shell.

  • Platforms. macOS through osascript, Linux through notify-send (it needs a notification daemon, which most desktops have). Windows has no banner yet; vibe --doctor says so when --notify is on and nothing can show one. The macOS command runs without error, but how the banner looks on screen has not been checked, and the Linux path has not been run.
  • Off by default, because a banner is more intrusive than a sound.
  • Safe by construction. The project name reaches the notifier as an argument, never as part of a script, because a directory name is whatever its owner called it.
  • Mute. --mute and VIBE_DISABLE silence banners with everything else.
  • Scripts. With it on, vibe --announce "deploy finished" shows its text as a banner too.

🔊 Spoken announcements: vibe --speak

A chime says that something finished, not which project. With --speak, the system voice adds one short sentence after the chime:

"api: done." · "api: failed." · "web needs you: Bash." · "payments looks stuck."

vibe --speak          # on: only when two or more sessions are going
vibe --speak-always   # on: every time, even with one session
vibe --no-speak       # off again (the default)

It is saved, so it applies from your next prompt with nothing to reinstall, and vibe --status shows the current setting on its speak line. The vibe menu has it too: Alerts & extras, then Enter on "Spoken announcements" steps through off, auto and always. VIBE_SPEAK=off turns it off for one shell.

To hear it once: vibe --speak-always, then run any agent turn (and vibe --unmute first if you muted).

  • Why only with two sessions by default. The sentence answers "which one?". With a single session the chime already told you, so --speak stays quiet and --speak-always is the opt-in for every time.
  • What it says. A project name and one of four fixed states. It never reads the agent's output, and it names a tool only if it is one of the built-in ones (Bash, Edit, ...), not an MCP tool whose name someone else chose. Names like my-app_v2 are spoken as "my app v2".
  • Voices. macOS say, Linux spd-say (or espeak-ng / espeak), Windows System.Speech through PowerShell. Nothing is installed by VibeAudio; with no voice present it stays silent, and vibe --doctor says so. Verified against the real macOS say and stub voices; not run on Linux or Windows.
  • Safety. The text reaches the voice as an argument (an environment variable on Windows), never as part of a shell command or script, and embedded speech commands such as [[volm 0]] are stripped.
  • Several at once. Sessions that finish together speak one after another, not over each other, and the same sentence twice within five seconds is said once. Not done: merging them into "api and web: done", and lowering the music while it speaks.
  • Muting. --mute and VIBE_DISABLE silence it with everything else. vibe --announce "deploy finished" text is spoken as well when speech is on.

As a plugin (Claude Code and Codex)

If Claude Code or Codex is the agent you use, the plugin installs the same hooks with no npm step. Inside a Claude Code session, type:

/plugin marketplace add kiril6/vibeaudio
/plugin install vibeaudio@vibeaudio

Or the same from a terminal:

claude plugin marketplace add kiril6/vibeaudio
claude plugin install vibeaudio@vibeaudio

For Codex:

codex plugin marketplace add kiril6/vibeaudio
codex plugin add vibeaudio@vibeaudio

Codex asks you to approve the plugin's hooks once, as it does for any hook. Update with codex plugin marketplace upgrade vibeaudio; remove with codex plugin remove vibeaudio@vibeaudio.

The plugin needs Node 18+ on your PATH (the hooks run node). In Claude Code it also adds /vibeaudio:vibe, the plugin's spelling of /vibe. Update with claude plugin update vibeaudio@vibeaudio; remove with claude plugin uninstall vibeaudio@vibeaudio (music already playing stops on its own within 15 minutes, or run /vibeaudio:vibe stop first).

Plugin details & coexistence with npm — settings file, coexistence, and limitations
  • Settings are the same file. vibe --genre jazz and the rest work as always, but the vibe command comes from npm i -g vibeaudio; the plugin alone puts nothing on your PATH. Use /vibeaudio:vibe genre jazz instead.
  • Both at once is safe. If --install-hooks has also been run, the plugin's hooks step aside and the installed ones play, so there's no double chime. Remove the installed hooks first if you want the plugin to own it.
  • Claude Code and Codex. Cursor, Gemini and the others still use vibe --install-hooks. Copilot CLI and Qwen Code can load the same plugin and it carries their events, but neither has been run with it yet.
  • Not reactive. --reactive is an --install-hooks option; the plugin doesn't carry it.

/vibe inside Claude Code

Installing the Claude Code hooks also adds a /vibe command (~/.claude/commands/vibe.md, or under $CLAUDE_CONFIG_DIR), so you can control the music without leaving the session:

/vibe                 what's installed and playing
/vibe mute 30         silence for 30 minutes (/vibe unmute to end it early)
/vibe stop            stop the music now
/vibe genre jazz      switch genre, keeping your volume
/vibe volume 20       change volume, keeping your genre

The optional /vibeaudio mod does the same instantly with no turn; /vibe is the version that works everywhere. It asks Claude to run the matching vibe command, so Claude Code will ask permission for that command the first time. If you already have your own vibe.md there, VibeAudio leaves it alone and says so; --uninstall-hooks removes only its own.

/vibeaudio: instant controls and a pane (Claude Code mod)

/vibe is a prompt, so Claude has to read it, run a command and answer: it costs a turn and a few tokens, and it waits for the current turn to end. The optional VibeAudio mod runs inside Claude Code instead, so /vibeaudio mute takes effect at once, even while Claude is working, and costs no tokens.

The VibeAudio band above the Claude Code prompt The VibeAudio pane: now playing, sound, alerts and share
▶ Watch the 43-second demo of the band and the pane (frames from the demo video, with sound on the site).

claude plugin marketplace add kiril6/vibeaudio
claude plugin install vibeaudio-mod@vibeaudio

Then /reload-plugins or a new session; /plugin shows 1 mod active.

To update later, run claude plugin marketplace update vibeaudio and then claude plugin update vibeaudio-mod@vibeaudio, followed by /reload-plugins. The mod does not update when the vibe command does.

/vibeaudio               open the pane: mute, stop, music, genre, preview, volume, speech, banners, announcements, test signal
/vibeaudio mute 30       silence for 30 minutes (unmute ends it)
/vibeaudio genre jazz    also: volume <n>, music on|off, speak off|auto|always, notify on|off, announce on|off, preview, stop
/vibeaudio genre zen here   genre, volume and music also take "here": save for this project only
/vibeaudio card          make your week card; the pane shows it in terminals that draw images
/vibeaudio band on       an always-visible logo band above the prompt (off by default; band off removes it)
/vibeaudio usage on      add context fill and plan limits (ctx 42%  5h 31%) to the band (off by default; usage off removes it)
/vibeaudio doctor        the setup check in a pane, each problem with its fix (vibe --doctor)
/vibeaudio help          every command on one line, and the "here" option

The pane is four cards under a logo header with a status pill (Playing, Ready, Muted or Music off): Now playing (mute for 15 minutes or an hour, stop the music, send a test signal), Sound (genre, volume, Preview plays one loop of the chosen genre, and where to save), Alerts (speech, banners, vibe --announce) and Share, which holds your last 7 days as a small chart and the week card buttons. Its colours come from the site and adapt to your Claude Code theme, light or dark. While you are muted, a line above the prompt says how many minutes are left, so a forgotten mute does not look like a broken tool. Nothing else is shown unless you turn the band on.

The optional band (/vibeaudio band on, remembered across sessions) puts the VibeAudio logo above the prompt, three level bars that move while music is playing (an SVG on the Desktop app, text bars in the terminal). Next to it: the genre and volume, your last 7 days as the week card counts them (turns and time waited, read from ~/.vibeaudio/history.jsonl). The bars also move while another agent on the machine is working. The buttons are compact icons that show the current state: 🔊/🔇 sound on or muted (press to flip it), ♪ on/off for music, ♫ with the current genre (press for the next one), − and + for volume, ▤ week for the week card (then ↗ to open it) and ⚙ for the controls pane. In a short window it shrinks to one line. It runs vibe --state every 5 seconds and redraws the text bars twice a second only in the terminal, while it is on and music is playing; on the Desktop app the SVG animates itself, so only the level bars inside the headphones move. Tab to it with Ctrl+X then Tab. With /vibeaudio usage on it also shows how full the context window is and, on a Claude plan, your 5-hour and 7-day limits, turning the warning colour at 80%; it reads the same figures Claude Code's status line has, and nothing else. While Claude works, the spinner also says what is playing (· ♪ jazz…, or · ♪ muted).

  • Per project. The pane's Saving button switches genre, volume and music between everywhere and this project, and the pane notes when this folder has its own settings (vibe --genre zen --here, found from a subfolder the way the CLI does). There is no way to remove a project's settings yet, in the mod or the CLI.
  • It changes nothing itself. Every action runs the vibe command with a fixed argument list (no shell), so settings stay in ~/.vibeaudio/config.json and the pane cannot disagree with the CLI. The one thing it keeps for itself is whether the band is on, in Claude Code's own store for the mod. It needs vibe on your PATH (npm i -g vibeaudio) and Claude Code 2.1.287 or later (Desktop app 2.1.286). It draws in the terminal and the Desktop app; in the VS Code panel and claude -p the same commands answer in text. It makes no network request.
  • A mod is code, not configuration. It runs inside Claude Code with your permissions and is not sandboxed, which is why --install-hooks never installs it for you; it prints the two commands until Claude Code lists the mod as installed. Read it first: it is mods/vibe/hooks/register.js, and claude plugin validate mods/vibe lists every call it makes (it reads three files in ~/.vibeaudio/ (config.json, muted, history.jsonl), your Claude Code theme, the session's usage figures (only with usage on), runs vibe, and draws).
  • /vibe stays, as the fallback. It is a prompt, so it costs a turn, but it works on every Claude Code version and surface, including where the mod does not load (older than 2.1.287, or a surface that skips mods). Use /vibeaudio where the mod loads and /vibe everywhere else; both change the same settings in ~/.vibeaudio/config.json. The vibe and vibeaudio commands in your shell are the same program. Remove the mod with claude plugin uninstall vibeaudio-mod@vibeaudio.

Changing the sound later

One command, and it reaches everything:

vibe --genre electronic --volume 25

That saves your default to ~/.vibeaudio/config.json. Installed hooks read it on their next prompt — nothing to reinstall, nothing to restart. The same file is what the wrapper and the MCP server use, so there is one answer to "what genre am I on" rather than three. vibe --status shows it.

Audition before you commit: vibe --preview electronic.

A flag still beats an environment variable, which still beats the saved file — so a one-off stays a one-off:

vibe --genre 8bit npm test      # this run only, nothing saved

Coming from an older install? Its genre and volume were frozen into the hook entries. The first vibe --install-hooks after upgrading carries them over into the config file, so nothing about your music changes — unless you name a new genre or volume, or have already saved one.

Reactive mode is the exception, because it isn't a setting the music reads — it decides which hooks exist at all. Turning it on or off means an install:

vibe --reactive --install-hooks     # on
vibe --install-hooks                # off again

A new genre (saved everywhere, not with --here) replaces the music playing right now while a turn is running, and so does --music off or on. A new volume eases in within a second. vibe --stop ends the music and the running turns.

Removing them is one command:

vibe --uninstall-hooks

Signals only: vibe --music off

VibeAudio does two jobs: music while an agent works, and signals (the done, failed and "needs you" chimes, banners, the stuck alert, --events). If you only want the second:

vibe --music off      # signals only
vibe --music on       # music back, in the genre you had
vibe --music off --here   # this project only

It's a setting of its own, saved to config.json like genre and volume, so your genre is still there when you turn music back on. Like the others, a flag beats VIBE_MUSIC=off, which beats --here, which beats the global value, and installed hooks pick it up on the next prompt.

With music off, hooks still track every turn: the chimes, --notify banners, --state, --events and --report behave exactly as before, and no background player is started. The wrapper (vibe npm test) stays silent while it runs and chimes when a run that outlasted the grace window ends, so it becomes "tell me when it's done". MCP's vibe_play tells the model that music is off rather than claiming it started. vibe --status and vibe --doctor show the setting.

Two things differ from music on. The stuck heartbeat is a layer on the music, so with music off it plays once on its own, the same two soft beats, when a session crosses into stuck (you also get the stuck event and, with --notify, the banner). And turning music off while something is playing doesn't cut it: vibe --stop does, or it ends with the current turn.

Know where your time goes: vibe --report

Hooks log each finished turn — which project, how long, how it ended, and how much of it the agent spent blocked on a dialog of yours — to ~/.vibeaudio/history.jsonl. vibe --report (or --report 30) totals it:

  Turns             30  3 failed
  You waited        3h 55m  on agents, 7m 51s per turn
  Agents waited     5m 35s  on you — permission dialogs and questions
  Typical turn      2m 57s  median
  Back to it        41s  median from done to your next prompt, 27 times
                    32s with a chime  1m 50s without (muted or --no-chime)
How the report metrics & return times are calculated

plus where the time went by project and, for windows up to two weeks, by day. "You waited" is the agent's own working time; the time it spent blocked on you is shown separately, because they are different problems. "Back to it" is the other direction: how long a finished turn sat before your next prompt in the same session — gaps over 30 minutes count as breaks and are left out. Once both sides have five turns, it is split by whether a chime announced the turn, so you can see what the chime is worth to you. The log stays on your machine, never leaves it, is capped at about 1 MB, and VIBE_NO_HISTORY=1 turns it off. Only hook-driven turns are logged — a command wrapped as vibe <command> is not, since its lifetime isn't the same thing as an agent's working time.

Share it: vibe --report --card (or --report 30 --card) saves the same totals as a square image to ~/.vibeaudio/card.png (--out <path> writes it elsewhere). It shows your time with agents, turns, typical and longest turn, a bar per day, the agents VibeAudio hooks into, and where to get it. It carries totals only — never a project name — and is written locally, never uploaded: attach it to a post yourself. The PNG needs macOS; elsewhere you get the SVG, which any browser opens. One file, replaced on each run.

The chime is in the music's key

The success chime is the tonic chord of whatever key your genre sits in — C major for lofi, 8bit, jazz and piano, D minor for synthwave and electronic, D major for zen, A minor for drone — so it lands as the resolution of the piece rather than a bell over it. rain, ocean and random have no single key to match and keep the original C major chime.

When an agent looks stuck

The worst stretch of agent work is twenty minutes of the same thing failing while the music says all is well. So when 4 of a session's last 8 tool calls fail, a soft heartbeat — two low beats about once a second, on the tonic of your genre — joins the music at the next loop boundary, and leaves once enough calls succeed. With --notify on, you also get one banner when the session crosses the line ("api: looks stuck"), not one per failure.

How the stuck threshold was calibrated (4,497 turns analyzed)

The threshold was picked against 4,497 real Claude Code turns (46,032 tool calls). It fires in about 1% of turns, at around minute 3, and those turns typically ran for another 3 minutes — time you could have spent stepping in. "3 in a row" was rejected because the usual loop has a successful edit between every failing test run. Each new prompt starts with a clean slate, and an interrupt (Esc) never counts as a failure.

It needs the agent to report failed tool calls, which Claude Code, Copilot CLI and Qwen Code do (PostToolUseFailure). Codex, Cursor, Gemini, Grok, Windsurf and Antigravity don't (Antigravity reports a tool error, but not a failing command), so for them the music never adds the heartbeat.

Reactive mode (opt-in)

vibe --reactive --install-hooks

Adds a PreToolUse hook so intensity follows what the agent is doing, not just how long it's taken:

Agent is… Tools Tier
Looking things up, or waiting on you Read, Grep, Glob, WebFetch, AskUserQuestion 1 — sparse
Changing code Edit, Write, NotebookEdit, apply_patch 2 — groove enters
Shelling out, or handing work to a subagent Bash, Agent, Skill, shell 3 — peak

Each agent names its tools differently, and all the vocabularies are mapped. MCP tools stay at tier 2 — they arrive as mcp__<server>__<tool> and can't be enumerated, since everyone's servers differ. That's a third of real traffic, and tier 2 is the honest read: real work, rarely the heaviest thing in a turn.

Changes land at the next loop boundary, so it shifts musically rather than cutting mid-bar. Without a tool signal it falls back to time-based escalation.

Off by default on purpose — music that moves every time the agent switches tools is music you notice, which is the opposite of what focus audio is for. Try it; if you catch yourself listening instead of reading, reinstall without --reactive and the PreToolUse hook goes away again.

What the installer will and won't do to your config — it edits a file you own, so here is the whole of it
  • Merges, never replaces. Other tools' hooks are left untouched and keep their position in the file — which is what Codex keys its trust records by.
  • Backs up first, once. The first install copies your file alongside as *.vibeaudio.bak. Later installs leave it alone — re-copying would overwrite your real pre-VibeAudio config with a copy of VibeAudio's own last install.
  • Reinstalling updates, never duplicates. Uninstalling sweeps every supported agent and removes only VibeAudio's own entries.
  • Refuses rather than clobbers. Malformed JSON aborts the write; a temporary npx checkout is rejected outright, since the hook records an absolute path that npm's cache eviction would later delete.
  • Can't run away. The background player stops once its sessions have been silent for 15 minutes (and after 4 hours at most), so a missed stop event can't leave music looping.

Build on it: vibe --state and vibe --events

The hard part of VibeAudio isn't the music. It's turning nine agents' different hook events into one set of states that mean the same thing everywhere. The music is one consumer of those states, and anything else can be another: a smart light, a menu bar icon, a tmux status line, a Stream Deck key.

vibe --state
# {"v":1,"status":"waiting","sessions":[{"session":"…","project":"/work/api","state":"waiting","since":1791026124771,"tool":"Bash"}]}

Each session is working, stuck (4 of its last 8 tool calls failed) or waiting (on you: a permission dialog or a question). status is the machine's overall state: the most urgent of its sessions, in the order waiting > stuck > working > idle. One session waiting on you matters more than three working.

vibe --events prints that snapshot once, then a JSON line for every change, as it happens:

{"v":1,"at":1791026130000,"event":"waiting","session":"…","project":"/work/api","tool":"Bash","status":"waiting"}

Events: started, waiting, resumed, stuck, recovered, finished (with outcome: success or failure), interrupted, ended (with reason when vibe --stop or --uninstall-hooks ended it) and announced (from vibe --announce, with text and outcome, and no session). Every line also carries status, the machine's state after the event, so a consumer that only cares about the overall state can read that one field.

📟 Status Bar & Desktop Integrations

vibe --statusline prints the same state as one plain line, with no jq, and nothing at all when idle, so a prompt segment disappears. It shows the most urgent state, with a count when several sessions are in it, and names the project when one is waiting on you:

working
working ×2
stuck
waiting: api

It is plain text on purpose: tmux and Starship do their own styling. It doesn't run ps and doesn't change when you're muted.

tmux status bar:

set -g status-right '#(vibe --statusline)'
set -g status-interval 5

tmux re-runs #(...) every status-interval seconds, 15 by default, so without the second line a dialog can wait up to 15 seconds before the bar says so.

macOS speech alert — announce out loud whenever an agent stops to ask for your input:

vibe --events | jq --unbuffered -r 'select(.event=="waiting") | .project' | while read p; do say "$(basename "$p") needs you"; done

Starship prompt — custom indicator in ~/.config/starship.toml:

[custom.vibe]
command = "vibe --statusline"
when = "command -v vibe >/dev/null"
format = "([🎧 $output ]($style))"

The parentheses matter: Starship hides a group whose variables are empty, so the 🎧 disappears with the text when idle. Put the icon in a symbol instead and it shows all the time. vibe --statusline takes about 40 ms, so running it on every prompt is fine.

Webhook — post waiting and stuck to anything that takes an HTTP request (a smart plug bridge, a Stream Deck plugin, Slack's incoming webhooks):

vibe --events | jq --unbuffered -c 'select(.event=="waiting" or .event=="stuck") | {event, project}' \
  | while read -r line; do curl -s -X POST -H 'Content-Type: application/json' -d "$line" https://example.com/hook; done

The select also drops the snapshot --events prints first (its event is state), so only changes after you start it are sent: a session already waiting at that moment is not announced. For the current state, call vibe --state once.

Home Assistant (or any smart light) — send the machine's overall state on every change, and let the receiving side choose the colour. Home Assistant takes this as a webhook trigger; replace the URL and the id:

vibe --events | jq --unbuffered -c 'select(.event != "state") | {status, event, project}' \
  | while read -r line; do curl -s -X POST -H 'Content-Type: application/json' -d "$line" http://homeassistant.local:8123/api/webhook/vibeaudio; done

status is idle, working, stuck or waiting, already the most urgent across all your sessions, so the automation is a plain switch on one field (for example amber for waiting, red for stuck, off for idle) and never has to count sessions itself. A web request goes out on every change, so point it at a local bridge rather than a metered service.

Not covered yet: a macOS menu bar plugin (SwiftBar/xbar) and Linux desktop alerts. Neither has been run on a real setup, so neither is documented. --notify already raises a banner when a turn finishes or needs you.

Verified on macOS arm64 with Node 24, tmux 3.7 and Starship 1.26 (the Home Assistant recipe: the request it sends, against a local stand-in server, was {"status":"waiting","event":"waiting","project":"/work/api"} on a permission dialog; not run against Home Assistant itself): the tmux and Starship segments showed working, working ×2 and waiting: web and disappeared when idle; the say and webhook pipelines fired once per waiting event while the stream stayed open. Not run on Linux or Windows.

The stream is a local file (~/.vibeaudio/events.jsonl, rotated at 256 KB), so it never leaves your machine. It keeps updating while you're muted, because a mute silences sound and a light isn't sound. The v field is the format version, and any breaking change will increment it.

🖥️ A live view: vibe --dashboard

vibe --dashboard        # serves http://127.0.0.1:4747 and opens it

The VibeAudio dashboard: one waiting, one stuck and one working session, with the recent events below

One row per session (waiting first, then stuck, then working), with the project, the tool it's waiting on and how long it has been in that state, and a feed of recent events with failures and stuck sessions in red. It's built on --events, so it shows every agent VibeAudio hooks into, and it updates as things happen.

It's read-only and local: it binds to 127.0.0.1 only, refuses any request whose Host isn't its own address (so a web page can't reach it through DNS rebinding), sends no CORS headers, and only answers GET, so there's nothing a page in another tab could trigger. It uses no external assets, and nothing leaves your machine. It runs in the foreground until Ctrl+C. --port <n> picks the port (4747 by default; if that's taken, a free one is used), and --no-open skips opening the browser.

🔔 Beyond agents: vibe --announce

A CI run, a deploy, a long script that wasn't started through vibe <command>: the same chimes can say those finished too.

vibe --announce "CI green"                       # success chime
vibe --announce "deploy" --outcome attention     # the "needs you" chime
./deploy.sh; vibe --announce "deploy" --exit-code $?     # 0 is success, anything else failure
gh run watch; vibe --announce "CI finished" --exit-code $?

It plays the chime for the outcome, shows the --notify banner and says the text aloud if you have those on, and appends an announced event (source: "cli", the text and the outcome) to ~/.vibeaudio/events.jsonl, so a light or dashboard built on --events reacts too. It never starts or stops music and never touches an agent session. A mute silences the sound and banner but not the event, like every event.

It always exits 0, so it can't turn a pipeline red, and the text is only ever passed as an argument, never through a shell. Text is capped at 200 characters, and the same text announced again within five seconds is dropped, so a loop can't machine-gun your speakers. --no-chime keeps it to the banner and event.

To switch it off for good, for a script you can't edit: vibe --announcements off (or VIBE_ANNOUNCE=off for one shell, or the Alerts & extras screen in the vibe menu). It then does nothing and still exits 0, without even writing the event. A mute is different: it silences the sound but keeps the event.

🖥️ Everything Else (Claude Desktop, VS Code… via MCP)

Claude Code, Codex, Cursor, Grok, Gemini CLI, Copilot CLI, Qwen Code Windsurf and Antigravity have hook systems, and --install-hooks writes to all nine — use hooks there, they're strictly better. This section is for everything else. MCP is the way in: it's plain stdio JSON-RPC, so the setup is identical everywhere and only the config file differs.

This is weaker than hooks, by nature. Hooks fire on an event; MCP tools are model-invoked, so the assistant has to decide to call vibe_play and remember vibe_stop. Expect the occasional silent turn — say "play some focus music while you work on this" if you want it reliably. On any of the nine agents above, use hooks instead.

Use an absolute path, not the bare vibe command: GUI apps launched from Finder don't inherit your shell's PATH, and version managers like fnm or nvm put vibe on a per-shell path that won't resolve. Print yours with:

echo "$(npm root -g)/vibeaudio/bin/vibeaudio.js"

Most apps use this JSON shape:

{
  "mcpServers": {
    "vibeaudio": {
      "command": "node",
      "args": ["/absolute/path/from/the/command/above", "--mcp"]
    }
  }
}

Where the file lives:

Tool Config file
Claude Desktop (macOS) ~/Library/Application Support/Claude/claude_desktop_config.json
VS Code, Zed, … that app's own MCP settings — same JSON shape

Restart the app afterwards.

Codex's TOML, and older Gemini CLI releases

Codex uses TOML rather than JSON, in ~/.codex/config.toml — but it has hooks, so use those instead unless you specifically want model-invoked music:

[mcp_servers.vibeaudio]
command = "node"
args = ["/absolute/path/from/the/command/above", "--mcp"]

Older Gemini CLI releases predate its hook system (0.10.0 has none) — if you're on one and can't upgrade, the same JSON goes in ~/.gemini/settings.json.

Changing the genre here

Set it like anywhere else. vibe --genre jazz --volume 25 saves the default the server reads, and it reads it per request — so the next vibe_play picks it up with no restart and no config edit, even though the app launched the server and you didn't.

Or just ask. vibe_play takes a genre, so "play some jazz while you work on this" is enough. A genre the assistant passes wins over the saved default.

Or pin one in the config's env block, if you want this client to differ from the rest of your machine — a GUI app won't inherit VIBE_GENRE from your shell. Most clients support env alongside command/args:

"env": { "VIBE_GENRE": "jazz", "VIBE_VOLUME": "25" }
[mcp_servers.vibeaudio.env]
VIBE_GENRE = "jazz"
VIBE_VOLUME = "25"

Restart the app afterwards — this one is read from the environment the server was launched with, so unlike the saved default it can't change under a running client.

Exposed MCP Tools:

  • vibe_play: Start procedural focus music (genre: lofi, synthwave, 8bit, electronic, jazz, zen, piano, drone, rain, ocean, random; volume: 5-100; project_dir: the workspace's absolute path, optional).
  • vibe_stop: Stop music and play the completion chime (outcome: success or failure).
  • vibe_status: Return current playback state and active tier, and which project the arrangement comes from.

Which project it sounds like. Hooks and the wrapper run inside your project, so each repo has its own arrangement. An MCP server is started once by the app, so it asks: a client that supports MCP roots (Claude Code does) reports its open folder, and any client's model can pass project_dir. That project's own settings apply too: a genre saved with vibe --genre jazz --here plays for that project over MCP as well. With neither, it uses the folder the app launched the server in, as before; VIBE_SEED still overrides all of it. vibe_status shows the project and its projectSource (project_dir, roots, cwd or VIBE_SEED).

Playback stops automatically if the desktop client disconnects, and caps out after 15 minutes — which also covers the likelier case of a model that started the music and never called vibe_stop.


🎨 Music Genres

VibeAudio includes 10 procedural sound styles synthesized entirely in code:

Genre Style Vibe
lofi ☕ Lo-Fi Focus (Default) Warm Rhodes electric piano chords & Kalimba drops
synthwave 🌌 Chill Synthwave '80s analog chorused pads & pulsing retro bass
8bit 🕹️ Cozy 8-Bit Filtered retro chiptune arpeggios & NES triangle bass
electronic ⚡ Melodic Electronic Downtempo resonant plucks & crisp tech pulse
jazz 🎷 Midnight Jazz Classic ii-V-I jazz piano chords & walking upright bass
zen 🎋 Zen Ambient Meditative Tibetan singing bowls & celestial drone (zero rhythm)
piano 🎹 Sparse Piano Single struck notes and long silences — Satie-ish
drone 🌫️ Deep Drone A held tone and filtered noise — no melody at all
rain 🌧️ Rain Band-passed rainfall with sparse droplets — no melody at all
ocean 🌊 Ocean Low surf on a slow swell that rises and drains — no melody at all
random 🎲 Shuffle Mode Picks a surprise genre for the run — never drone, rain or ocean
Genre aliases — you can ask for a genre the way you'd say it (chill, retrowave, bossa, ambient…)
Canonical Also accepted
lofi lo-fi, lofi-hiphop, chill, chillhop, study
synthwave retrowave, outrun, 80s
8bit chiptune, chip, nes, gameboy, retro
electronic downtempo, techno, edm
jazz bossa, swing, lounge
zen ambient, calm, meditation
piano sparse, satie, keys, minimal
drone noise, focus, hum, whitenoise, white-noise
rain storm, drizzle
ocean waves, sea, surf

Matching is case-insensitive, so BOSSA works too.

If any melody distracts you, use drone. Every other genre plays something — notes, a progression, a bass line — and some people can't read while that happens. drone holds one low tone under a slow-breathing noise bed and never moves: closer to a fan or rainfall than to music. Tiers add weight rather than movement. If you want the fan-and-rainfall idea literally, rain and ocean are synthesized the same way (seeded noise, no recorded files): rain adds more droplets as the turn runs longer, ocean adds a second swell and then foam.

piano is the gentler version of that idea. It still plays notes — two in eight seconds at tier 1 — but they're single struck tones with silence between them and nothing running underneath. Higher tiers fill the gaps rather than adding a groove. Try it before drone if you want something there.

For the same reason random never picks drone, rain or ocean. Shuffle is for a surprise mood, and drone isn't one — landing on a fan noise when you asked for variety reads as broken audio, not as range. Ask for it by name (or noise / focus) when you want it.

Usage Examples:

vibe --genre electronic claude   # Melodic Electronic
vibe --genre jazz npm test       # Midnight Jazz
vibe --genre zen claude          # Zen Ambient (no drums/rhythm)
vibe --genre random claude       # Surprise vibe each run

⚙️ Set Your Favorite Genre as Default

vibe --genre jazz --volume 25

Saved to ~/.vibeaudio/config.json and read by all three ways of running VibeAudio — wrapper, agent hooks, MCP. It applies on your next prompt.

vibe --genre jazz          # lofi, synthwave, 8bit, electronic, jazz, zen, piano, drone, rain, ocean, random
vibe --volume 25           # or --whisper / --quiet / --loud
vibe --chime-volume 70
vibe --status              # what's saved, and what's overriding it

An export VIBE_GENRE=jazz still works and takes precedence in that shell — useful for one terminal you want different, not needed for a default any more.

One project that should sound different:

cd ~/work/api
vibe --genre zen --here      # this directory and everything under it

Your global default is untouched, and an agent launched from a subdirectory still gets it — the lookup walks up, so ~/work/api/src finds what you saved at ~/work/api. vibe --status says which one answered (saved for this project / saved / default).

The full order, highest first: a flag → an environment variable → --here → your global default → lofi at 40%.


⚙️ Options & Flags

Flag Description Default
-g, --genre <name> Music style: lofi, synthwave, 8bit, electronic, jazz, zen, piano, drone, rain, ocean, random. With no command after it, saves your default lofi
-v, --volume <5-100> Set playback volume. With no command after it, saves your default 40
-cv, --chime-volume <5-100> Set independent completion chime volume volume × 1.1, kept within 35–65
--grace <ms> Silence window before music starts 1500
--here With a saved setting: this directory tree only, not everywhere off
--seed <n> Force a specific arrangement derived from the project directory
--music <on|off> off keeps the chimes, banners and events and drops the music. With no command after it, saves it as a setting. --here for one project on
--whisper Quick preset: 15% volume (headphones / late night) —
--quiet Quick preset: 25% volume (focus / open office) —
--loud Quick preset: 75% volume (hear from across the room) —
--no-chime Disable the resolution completion chime false
--no-hud Disable terminal window/tab title animation false
--preview <genre> Play one loop of a genre and exit —
--render [file] Write this project's music to a .wav and exit (full scale, ignores --volume) vibeaudio-<genre>.wav
--status Say why the music is silent or playing, then show what's installed, running and detected, and exit —
--doctor Check the setup; every problem comes with the command that fixes it. Exits 1 on a failure, so it scripts —
--notify / --no-notify Also show a desktop banner naming the project when a turn finishes, fails or needs you. Saved to config.json off
--speak / --speak-always / --no-speak Say which project finished, failed or needs you, after the chime: only with two or more sessions going (--speak), or every time (--speak-always). Saved to config.json off
--announcements <on|off> Turn vibe --announce on or off for good. Saved to config.json on
--report [days] How long you waited on agents, and on which projects, from the local turn log 7 days
--card [--out <path>] With --report: the totals as a shareable image - PNG on macOS, SVG anywhere - saved to ~/.vibeaudio/card.png, replaced each run off
--statusline The same state as one plain line for tmux or a shell prompt (working ×2, waiting: api); prints nothing when idle —
--state What every agent on the machine is doing, as one line of JSON: idle, working, stuck or waiting —
--events Stream agent state changes as JSON lines, starting with the current state, until stopped —
--dashboard Serve a read-only live view of every agent on this machine at http://127.0.0.1:4747, and open it. --port <n> and --no-open adjust it —
--announce <text> Play the chime for something that isn't an agent (CI, a deploy, a script), then exit. Add --outcome success|failure|attention or --exit-code $? success
--stop Stop the background player, then exit —
--mute [minutes] Silence everything for a call, then exit 60 min (0 = until unmuted)
--unmute Resume normal playback, then exit; a turn still running gets its music back at once —
--clear-cache Delete all cached audio, then exit —
--mcp Run as an MCP stdio server for desktop apps —
--install-hooks Wire music into your agent's hooks (no wrapper needed) —
--tools <list> With --install-hooks: claude,codex,cursor,grok,gemini,copilot,qwen,windsurf,antigravity auto-detect
--reactive With --install-hooks: intensity follows the tool in use off
--dry-run With --install-hooks: show what would change in each file, write nothing off
--uninstall-hooks Remove the hooks again, from every agent —
-h, --help Show help and options —
--version Show version —

⚙️ Environment Variables

Defaults live in ~/.vibeaudio/config.json now — vibe --genre jazz is the short way to set one. These override it for a single shell, which is what you want for one terminal that should sound different, or for a machine you don't want writing config at all:

export VIBE_GENRE=jazz          # lofi, synthwave, 8bit, electronic, jazz, zen, piano, drone, rain, ocean, random
export VIBE_VOLUME=25           # Background music at 25%
export VIBE_CHIME_VOLUME=70     # Crisp completion chime at 70%
export VIBE_GRACE_MS=3000       # Wait 3s of thinking before any music
export VIBE_SEED=7              # Same arrangement everywhere, ignoring the directory
export VIBE_DISABLE=1           # Mute, without uninstalling anything
export VIBE_MUSIC=off           # Signals only: chimes and banners, no music, for this shell
export VIBE_NOTIFY=1            # Desktop banner naming the project, for this shell
export VIBE_SPEAK=auto           # Spoken announcements (auto, always or off), for this shell
export VIBE_ANNOUNCE=off         # Make vibe --announce do nothing, for this shell
export VIBE_NO_HISTORY=1        # Do not log finished turns (what --report reads)
export VIBE_NO_UPDATE_CHECK=1   # Never ask npm whether a newer version is out
export VIBE_NO_FADE=1           # macOS: play with plain afplay, no fade in/out

VIBE_DISABLE=1 is for a shell you always want quiet — a CI job, a shared machine, a terminal profile you keep silent. It's read at playback time and covers hooks, wrapper and MCP alike.

For a call that's ringing right now, use vibe --mute instead — an exported variable can't reach a hook that's already running.

vibe --mute        # silent now, music returns by itself in an hour
vibe --mute 15     # or pick the window
vibe --mute 0      # stay off until I say otherwise
vibe --unmute      # end it early

It stops whatever is playing on the spot, leaves your hooks and settings untouched, and expires after an hour so you can't forget it. --preview still plays either way — that one is an explicit request to hear something.

Why a file, and why it expires

A hook runs as a child of your agent and inherits the environment the agent had when it launched, so exporting VIBE_DISABLE in another terminal reaches nothing already running — and restarting your agent is exactly what you can't do mid-call. --mute writes a flag file, which crosses that boundary where a variable cannot.

The expiry is deliberate too. A call is a bounded thing; a mute you forget about is worse than no mute, because the tool just stops working and nothing ever tells you why. Expiring means the worst case is "music came back sooner than I wanted" rather than a week of silence you never diagnose. --mute 0 opts into indefinite explicitly, and vibe --status always shows how much of the window is left.


🔧 Troubleshooting

Start with vibe --status. Its first line answers "why is it silent?" or "why is it still playing?" in one sentence, with the command that changes it: a mute and how long it has left, VIBE_DISABLE in your shell, no audio player, music turned off (globally, for this project, or by VIBE_MUSIC), a session waiting on you, or simply no agent working right now.

No sound at all Check that a player exists for your platform (see Requirements). VibeAudio prints a notice to stderr when it can't find one. On Linux: sudo apt install pulseaudio-utils (or ffmpeg / alsa-utils).

Silent after connecting a monitor or headphones (macOS) afplay plays on macOS's default output device, which moves when a display or headphones connect. A monitor often has no system volume control at all and speakers much quieter than the laptop's, so a quiet setting — a sparse genre like piano at 25% — can fall below what you can hear. vibe --doctor shows where sound is going and warns if that output is muted or has no volume control. Then try vibe --genre lofi --volume 60 for one prompt, and check the monitor's own speaker volume.

Music starts immediately instead of after the grace window It usually is waiting — your AI tool's own startup (auth, session load) just takes longer than 1.5s, so music and the tool's first output appear together. Raise the window: vibe --grace 3000 claude.

Music sounds stale after upgrading It shouldn't. The cache key includes a hash of the synth sources, so changing a generator invalidates it automatically and the old directory is pruned on the next run. To force a rebuild anyway:

vibe --clear-cache
Silent when the agent runs on another machine over SSH (socket forwarding)

Sound plays on the machine where the agent runs, and a remote server usually has no sound card — so VibeAudio finds no player and stays quiet. You can hear it locally by forwarding a PulseAudio socket through the SSH connection: the remote paplay sends the audio back over SSH to your own speakers.

This needs a PulseAudio-compatible sound server on your local machine: a Linux desktop (PipeWire and PulseAudio both provide one) or Windows with WSLg. macOS has none built in, so this route doesn't apply there.

  1. Connect with the socket forwarded. On the local machine, pick the socket for your setup:

    ssh -R /tmp/vibe-pulse.sock:"$XDG_RUNTIME_DIR/pulse/native" you@server   # Linux desktop
    ssh -R /tmp/vibe-pulse.sock:/mnt/wslg/PulseServer you@server              # WSLg
  2. On the server, point audio at it and install a player — in the shell you launch the agent from, since hooks inherit the agent's environment:

    sudo apt install pulseaudio-utils           # provides paplay
    export PULSE_SERVER=unix:/tmp/vibe-pulse.sock
    vibe --preview jazz                         # you should hear it locally

If the second connection fails with the socket "already in use", an earlier session left it behind: rm /tmp/vibe-pulse.sock on the server, or set StreamLocalBindUnlink yes in the server's sshd_config. If paplay says access denied, your local PulseAudio requires its cookie — copy ~/.config/pulse/cookie to the same path on the server.

Volume flag does nothing Shouldn't happen any more — where the player can't attenuate (aplay, PowerShell), the gain is baked into the audio instead. A --volume change reaches music already playing within a second on macOS hooks (it eases there), at the next loop boundary elsewhere, and in the wrapper or MCP on their next run; vibe --status shows the volume in effect and what set it.


🩺 What's actually running

vibe --status

Reads live state rather than guessing — the fastest answer to "why do I hear nothing" or "which genre is this set to":

Audio
  player    afplay
  cache     7.6 MB in ~/.vibeaudio/cache

Hooks
  Claude Code
    ✔ Stop
    ✔ UserPromptSubmit     jazz @ 25%, reactive
    ✔ PreToolUse
    ✔ PermissionRequest
    ✔ PostToolUse          jazz @ 25%, reactive
    ✔ StopFailure
    ✔ SessionEnd
  Codex        not installed — run: vibe --install-hooks
  Cursor       not installed — run: vibe --install-hooks
  Grok         not installed (not found on this machine)

Background player
  running pid 59078   stop it with: vibe --stop

AI tools found
  ✔ Claude Code         hooks — installed
  ✔ Codex               hooks — run: vibe --install-hooks
  ✔ Gemini CLI          hooks — run: vibe --install-hooks
  ✔ GitHub Copilot CLI  hooks — run: vibe --install-hooks

The last block is detected from your own PATH, so it answers "will this work with my tool" without you matching yourself against a table. vibe --stop kills the background player on the spot; the next prompt starts a fresh one.


🧹 Uninstall

Remove the hooks first, while vibe still exists:

vibe --uninstall-hooks          # 1. unwire every agent, and stop any player still running
npm rm -g vibeaudio             # 2. remove the CLI
rm -rf ~/.vibeaudio             # 3. optional: cached audio, saved settings, daemon state
Why that order, and what stays behind

Step 1 also stops a background player that's still going. That matters: once the hooks are gone nothing will ever send the Stop event, and after step 2 there's no vibe left to stop it with — music would simply play on until its 15-minute cap. If you ever need to do it by hand: pkill -f "vibeaudio.js --daemon".

Order matters. npm rm -g deletes the binary but not your hook config. Removing the package first strands hook entries that point at a path that no longer exists, and your agent will run a failing hook on every prompt. If you already did it in the wrong order, reinstall, run vibe --uninstall-hooks, then remove again — or delete the vibeaudio entries from ~/.claude/settings.json, ~/.codex/hooks.json, ~/.cursor/hooks.json, ~/.gemini/settings.json, ~/.qwen/settings.json and ~/.codeium/windsurf/hooks.json by hand, and delete ~/.grok/hooks/vibeaudio.json, ~/.copilot/hooks/vibeaudio.json and ~/.claude/commands/vibe.md.

Step 3 reclaims disk — the audio cache, pruned to the 3 most recent projects — and drops your saved genre and volume. Both come back on their own, so skip it if you're reinstalling.

Two things are deliberately left behind:

Leftover Why, and how to remove it
*.vibeaudio.bak next to each shared hook config Your config as it was before the first install — one each for Claude Code, Codex, Cursor, Gemini CLI, Qwen Code, Windsurf and Antigravity (if you had a hooks.json there). A safety net we won't delete for you; rm them once you're happy the real files are correct, and vibe --uninstall-hooks prints the path of every one it finds. (Grok and Copilot CLI leave nothing: their files are ours alone, so uninstall deletes them outright.)
vibeaudio entries in other apps' MCP configs VibeAudio never edits those files, so it can't clean them either. Drop the entry from whichever config you added it to.

Apart from those two, the three commands above remove everything VibeAudio writes.


🤝 Contributing

Contributions welcome — see CONTRIBUTING.md for the ground rules (zero runtime dependencies, no build step, pure synth modules) and the dev loop. Found a bug? Open an issue with your OS, Node version, and which audio player you have installed.

Working on it locally:

git clone https://github.com/kiril6/vibeaudio.git
cd vibeaudio
npm link        # puts your clone's `vibe` on PATH
npm test

💛 Supporting the Project

If VibeAudio is useful to you, consider giving it a ⭐ — it helps others find it.

Support development: ko-fi


📄 License

MIT © 2026

VibeAudio is not affiliated with or endorsed by Claude Code, Codex, Cursor, Grok, Gemini CLI, GitHub Copilot CLI, Qwen Code, Windsurf, Antigravity or any other tool it works with. Their names are used only to say what it works with.

About

Procedural focus music that follows your AI coding agent: it plays while the agent works, pauses when it needs you, and chimes when it's done. Zero dependencies, no audio files.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages