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, thenvibe --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, thencodex plugin add vibeaudio@vibeaudio(details).
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 --volumereaches 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 offkeeps 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 (
130onCtrl+C), sovibe claude && next-stepbehaves 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.
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 everywhereWhy 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.
vibe --render # -> vibeaudio-lofi.wav, ~53s
vibe --genre jazz --render our-api.wavA 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.
- 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.
One line. No clone, no build step, no dependencies to resolve:
npm i -g vibeaudioThat 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/vibeaudioSame 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.
Using Claude Code, Codex, Cursor or Grok interactively? Install the hooks — this is the mode that actually tracks thinking:
vibe --install-hooksWant the music to follow what the agent is doing — calm while it reads, busiest while it runs commands? Add --reactive:
vibe --reactive --install-hooksPrefer 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.
npx vibeaudio --preview jazz # hear a loop right now
npx vibeaudio sleep 8 # hear the wrapper: music, then the done chimeHeard 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 8is the reliable demo; something likenpm testis 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.
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.
--install-hookssupports 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 nothingIt 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
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 --notifyadds a desktop banner (api: finished,web: needs you (Bash)) andvibe --speakadds a spoken line. Both are off by default. Banner details below. -
Say it out loud.
vibe --speakadds 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.
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 throughnotify-send(it needs a notification daemon, which most desktops have). Windows has no banner yet;vibe --doctorsays so when--notifyis 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.
--muteandVIBE_DISABLEsilence banners with everything else. - Scripts. With it on,
vibe --announce "deploy finished"shows its text as a banner too.
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
--speakstays quiet and--speak-alwaysis 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 likemy-app_v2are spoken as "my app v2". - Voices. macOS
say, Linuxspd-say(orespeak-ng/espeak), WindowsSystem.Speechthrough PowerShell. Nothing is installed by VibeAudio; with no voice present it stays silent, andvibe --doctorsays so. Verified against the real macOSsayand 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.
--muteandVIBE_DISABLEsilence it with everything else.vibe --announce "deploy finished"text is spoken as well when speech is on.
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@vibeaudioFor Codex:
codex plugin marketplace add kiril6/vibeaudio
codex plugin add vibeaudio@vibeaudioCodex 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 jazzand the rest work as always, but thevibecommand comes fromnpm i -g vibeaudio; the plugin alone puts nothing on yourPATH. Use/vibeaudio:vibe genre jazzinstead. - Both at once is safe. If
--install-hookshas 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.
--reactiveis an--install-hooksoption; the plugin doesn't carry it.
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.
/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.
▶ 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
vibecommand with a fixed argument list (no shell), so settings stay in~/.vibeaudio/config.jsonand 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 needsvibeon yourPATH(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 andclaude -pthe 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-hooksnever installs it for you; it prints the two commands until Claude Code lists the mod as installed. Read it first: it ismods/vibe/hooks/register.js, andclaude plugin validate mods/vibelists 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 withusage on), runsvibe, and draws). /vibestays, 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/vibeaudiowhere the mod loads and/vibeeverywhere else; both change the same settings in~/.vibeaudio/config.json. Thevibeandvibeaudiocommands in your shell are the same program. Remove the mod withclaude plugin uninstall vibeaudio-mod@vibeaudio.
One command, and it reaches everything:
vibe --genre electronic --volume 25That 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 savedComing 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 againA 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-hooksVibeAudio 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 onlyIt'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.
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 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.
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.
vibe --reactive --install-hooksAdds 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
npxcheckout 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.
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.
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 5tmux 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"; doneStarship 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; doneThe 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; donestatus 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.
vibe --dashboard # serves http://127.0.0.1:4747 and opens itOne 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.
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.
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_playand remembervibe_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.
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.
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:successorfailure).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.
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.droneholds 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,rainandoceanare 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.
pianois 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 beforedroneif you want something there.For the same reason
randomnever picksdrone,rainorocean. 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 (ornoise/focus) when you want it.
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 runvibe --genre jazz --volume 25Saved 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 itAn 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 itYour 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%.
| 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 | — |
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/outVIBE_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 earlyIt 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.
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-cacheSilent 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.
-
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
-
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.
vibe --statusReads 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.
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 stateWhy 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 -gdeletes 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, runvibe --uninstall-hooks, then remove again — or delete thevibeaudioentries from~/.claude/settings.json,~/.codex/hooks.json,~/.cursor/hooks.json,~/.gemini/settings.json,~/.qwen/settings.jsonand~/.codeium/windsurf/hooks.jsonby hand, and delete~/.grok/hooks/vibeaudio.json,~/.copilot/hooks/vibeaudio.jsonand~/.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.
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 testIf VibeAudio is useful to you, consider giving it a ⭐ — it helps others find it.
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.

