/debug-voice
Use when the user runs /debug-voice, says voice mode has flaws, asks to see or capture what happened in a Grok realtime voice session, or a voice integration has no debug logging yet. Proposes a plan, then installs a dev-only log pipeline (client logger → local NDJSON) in the
Claude Code
$ npx -y skills add cursor/plugins --skill debug-voice --agent claude-code
Codex
$ npx skills add cursor/plugins --skill debug-voice -a codex -y
Cursor
$ npx skills add cursor/plugins --skill debug-voice -a cursor -y
Gemini CLI
$ npx skills add cursor/plugins --skill debug-voice -a gemini-cli -y
GitHub Copilot
$ npx skills add cursor/plugins --skill debug-voice -a github-copilot -y
OpenCode
$ npx skills add cursor/plugins --skill debug-voice -a opencode -y
Windsurf
$ npx skills add cursor/plugins --skill debug-voice -a windsurf -y
SKILL.md
debug-voice.SKILL.mdname: debug-voice
description: >-
Use when the user runs /debug-voice, says voice mode has flaws, asks to see or
capture what happened in a Grok realtime voice session, or a voice integration
has no debug logging yet. Proposes a plan, then installs a dev-only log
pipeline (client logger → local NDJSON) in the app's own language and
conventions, then runs the fix loop: match the user's report to log
signatures, fix one thing, re-test.
Debug Voice
Make a voice session readable after the fact, then fix from evidence. The agent cannot hear the app; the log is its ears, the user is its judge. No audio, no tokens, never in prod.
Works for any stack. The pipeline is a small contract (below); implement it in whatever the app already uses.
Workflow
1. **Map** the app (read only). 2. **Plan**: write the change list, show it, **stop**. Nothing is edited until the user aligns. 3. **Install** the agreed pieces in the app's language, framework, and conventions. 4. **Verify** the sink, hand the app to the user. 5. **Fix loop** from the log.
1. Map
Find, and note the paths:
- Voice client: where realtime events are received and sent, mic capture, audio playback, token fetch.
- Server: framework, how routes are declared, where shared server code lives, how env is read, what "production" means here.
- Conventions: language(s), module system, formatter, where scripts or tasks live (`package.json`, `Makefile`, `pyproject`, `justfile`), `.gitignore`.
- Client kind: browser, mobile, desktop, CLI. A non-browser client still POSTs the same JSON; a single-process app can skip HTTP and append to the file directly.
- Where audio deltas are handled. They must be counted, never logged.
2. Plan, then stop
Fill this in with real paths and the app's language, post it, and wait for a yes or a trimmed list. Do not edit files before that.
## Debug voice: plan
Add
- <path>: client logger (batch, redact, flush) in <language>
- <path>: dev-only sink `POST /api/voice/log` → `.voice-logs/<sessionId>.ndjson`
- <path> (optional): summary command `<cmd>`; otherwise read the NDJSON with jq
Modify
- <voice client file>: hook points start, token, mic, env, ws.*, client/server events,
audio.in (2 s windows), audio.out.first, audio.out, play.stop, stop
- <token route>: append `server.token { ok, status, ms, upstream }` (never the token)
- <UI file>: session id in the voice status line and in voice error messages
- .gitignore: `/.voice-logs`
- <scripts file>: a `voice:logs` task (only if the summary command is wanted)
Logged: event names and non-audio fields, timings, byte counts, mic RMS.
Never: tokens, API keys, raw audio, strings over 400 chars.
Off in production unless `VOICE_LOG=1`.
Reply "go", or strike lines you do not want.3. Install: the contract
Match the app. Same language as the surrounding code, same route style, same formatter. Write the pieces from the contract below; do not introduce a second language or toolchain for logging.
**Session id**: 8 lowercase hex chars from a UUID. The sink accepts `^[a-z0-9]{4,64}$`; it becomes a file name.
**Entry** (one JSON object per line):
| Field | Client | Server | | --- | --- | --- | | `t` | ms since the logger started | absent; the reader aligns by `ts` | | `ts` | epoch ms | epoch ms | | `kind` | `start`, `server`, `client`, `error`, `audio.in`, … | `server.token`, … | | `src` | absent | `"server"` | | rest | the hook's fields, redacted | the hook's fields |
**Redaction, applied client side before buffering**: on audio event types (`response.output_audio.delta`, `response.audio.delta`, `input_audio_buffer.append`) replace `delta` / `audio` with `bytes` = decoded base64 length; strings over 400 chars cut to 400 + `…[N chars]`; objects deeper than 4 → `"[depth]"`; arrays over 50 items truncated.
**Client logger**: buffer entries; flush as `POST <sink> {"sessionId","entries":[…]}` every 1 s or at 200 entries; on stop flush with keepalive (or the platform's "survive navigation" equivalent); swallow every transport error, logging must never throw into the voice path. Also mirror entries to the console in dev.
**Sink**: `POST /api/voice/log`, JSON body. `404` unless dev or `VOICE_LOG=1`. `400` if `sessionId` fails the regex or `entries` is not an array. Append at most 500 entries per request, drop any line over 16,000 chars, to `.voice-logs/<sessionId>.ndjson`, creating the directory. Reply `204`.
Pseudocode for any server:
handle POST /api/voice/log:
if production and VOICE_LOG != "1": return 404
body = parse json or return 400
if not regex(body.sessionId) or not list(body.entries): return 400
mkdir .voice-logs; append join(json(e) for e in body.entries[:500] if len < 16000) to .voice-logs/{sessionId}.ndjson
return 204Pseudocode for the client logger:
logger(sessionId, sink):
buffer = []; started = now()
log(kind, data): buffer.push({ ...redact(data), t: now() - started, ts: epoch_ms(), kind }); schedule flush (1 s timer, or immediately at 200 entries)
server(event, extra): log("server", { ...redact_event(event), ...extra }) # never per audio delta
client(event): log("client", redact_event(event)) # never per audio chunk
error(where, err, extra): log("error", { where, name, message, ...extra })
flush(final=false): POST sink {"sessionId","entries": buffer}; buffer = []; ignore all errors; keepalive when final
close(): flush(final=true)Hook points
`kind` and fields; the shape is the same in every language.
| When | `kind` and fields | | --- | --- | | Session start | `start { url, target_rate }` | | Token fetched / failed | `token.ok { ms }` / `error { where: "token", name, message, ms }` | | Mic granted / denied | `mic.ok { ms, label, settings }` / `error { where: "mic", … }` | | Audio graph ready | `env { ua, mic_rate, mic_state, play_rate, play_state, capture_frames, target_rate }` | | Socket | `ws.connecting`, `ws.open { ms }`, `ws.
Read more
name: debug-voice description: >- Use when the user runs /debug-voice, says voice mode has flaws, asks to see or capture what happened in a Grok realtime voice session, or a voice integration has no debug logging yet. Proposes a plan, then installs a dev-only log pipeline (client logger → local NDJSON) in the app's own language and conventions, then runs the fix loop: match the user's report to log signatures, fix one thing, re-test.
Debug Voice
Make a voice session readable after the fact, then fix from evidence. The agent cannot hear the app; the log is its ears, the user is its judge. No audio, no tokens, never in prod.
Works for any stack. The pipeline is a small contract (below); implement it in whatever the app already uses.
Workflow
1. **Map** the app (read only). 2. **Plan**: write the change list, show it, **stop**. Nothing is edited until the user aligns. 3. **Install** the agreed pieces in the app's language, framework, and conventions. 4. **Verify** the sink, hand the app to the user. 5. **Fix loop** from the log.
1. Map
Find, and note the paths:
- Voice client: where realtime events are received and sent, mic capture, audio playback, token fetch.
- Server: framework, how routes are declared, where shared server code lives, how env is read, what "production" means here.
- Conventions: language(s), module system, formatter, where scripts or tasks live (`package.json`, `Makefile`, `pyproject`, `justfile`), `.gitignore`.
- Client kind: browser, mobile, desktop, CLI. A non-browser client still POSTs the same JSON; a single-process app can skip HTTP and append to the file directly.
- Where audio deltas are handled. They must be counted, never logged.
2. Plan, then stop
Fill this in with real paths and the app's language, post it, and wait for a yes or a trimmed list. Do not edit files before that.
## Debug voice: plan
Add
- <path>: client logger (batch, redact, flush) in <language>
- <path>: dev-only sink `POST /api/voice/log` → `.voice-logs/<sessionId>.ndjson`
- <path> (optional): summary command `<cmd>`; otherwise read the NDJSON with jq
Modify
- <voice client file>: hook points start, token, mic, env, ws.*, client/server events,
audio.in (2 s windows), audio.out.first, audio.out, play.stop, stop
- <token route>: append `server.token { ok, status, ms, upstream }` (never the token)
- <UI file>: session id in the voice status line and in voice error messages
- .gitignore: `/.voice-logs`
- <scripts file>: a `voice:logs` task (only if the summary command is wanted)
Logged: event names and non-audio fields, timings, byte counts, mic RMS.
Never: tokens, API keys, raw audio, strings over 400 chars.
Off in production unless `VOICE_LOG=1`.
Reply "go", or strike lines you do not want.3. Install: the contract
Match the app. Same language as the surrounding code, same route style, same formatter. Write the pieces from the contract below; do not introduce a second language or toolchain for logging.
**Session id**: 8 lowercase hex chars from a UUID. The sink accepts `^[a-z0-9]{4,64}$`; it becomes a file name.
**Entry** (one JSON object per line):
| Field | Client | Server | | --- | --- | --- | | `t` | ms since the logger started | absent; the reader aligns by `ts` | | `ts` | epoch ms | epoch ms | | `kind` | `start`, `server`, `client`, `error`, `audio.in`, … | `server.token`, … | | `src` | absent | `"server"` | | rest | the hook's fields, redacted | the hook's fields |
**Redaction, applied client side before buffering**: on audio event types (`response.output_audio.delta`, `response.audio.delta`, `input_audio_buffer.append`) replace `delta` / `audio` with `bytes` = decoded base64 length; strings over 400 chars cut to 400 + `…[N chars]`; objects deeper than 4 → `"[depth]"`; arrays over 50 items truncated.
**Client logger**: buffer entries; flush as `POST <sink> {"sessionId","entries":[…]}` every 1 s or at 200 entries; on stop flush with keepalive (or the platform's "survive navigation" equivalent); swallow every transport error, logging must never throw into the voice path. Also mirror entries to the console in dev.
**Sink**: `POST /api/voice/log`, JSON body. `404` unless dev or `VOICE_LOG=1`. `400` if `sessionId` fails the regex or `entries` is not an array. Append at most 500 entries per request, drop any line over 16,000 chars, to `.voice-logs/<sessionId>.ndjson`, creating the directory. Reply `204`.
Pseudocode for any server:
handle POST /api/voice/log:
if production and VOICE_LOG != "1": return 404
body = parse json or return 400
if not regex(body.sessionId) or not list(body.entries): return 400
mkdir .voice-logs; append join(json(e) for e in body.entries[:500] if len < 16000) to .voice-logs/{sessionId}.ndjson
return 204Pseudocode for the client logger:
logger(sessionId, sink):
buffer = []; started = now()
log(kind, data): buffer.push({ ...redact(data), t: now() - started, ts: epoch_ms(), kind }); schedule flush (1 s timer, or immediately at 200 entries)
server(event, extra): log("server", { ...redact_event(event), ...extra }) # never per audio delta
client(event): log("client", redact_event(event)) # never per audio chunk
error(where, err, extra): log("error", { where, name, message, ...extra })
flush(final=false): POST sink {"sessionId","entries": buffer}; buffer = []; ignore all errors; keepalive when final
close(): flush(final=true)Hook points
`kind` and fields; the shape is the same in every language.
| When | `kind` and fields | | --- | --- | | Session start | `start { url, target_rate }` | | Token fetched / failed | `token.ok { ms }` / `error { where: "token", name, message, ms }` | | Mic granted / denied | `mic.ok { ms, label, settings }` / `error { where: "mic", … }` | | Audio graph ready | `env { ua, mic_rate, mic_state, play_rate, play_state, capture_frames, target_rate }` | | Socket | `ws.connecting`, `ws.open { ms }`, `ws.
Official Cursor plugins for popular developer tools, frameworks, and SaaS products. Each plugin is a standalone directory at the repository root with its own .cursor-plugin/plugin.json manifest.
More skills from other plugins.
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…
ask-matt
Ask which skill or flow fits your situation. A router over the skills in this repo.
Questions about debug-voice.
- Which coding agents can use debug-voice?
- debug-voice installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.

