Skip to content
Back to skills

Elevenlabs Mod

ASecurity

Call ElevenLabs from inside a Claude Code mod - text-to-speech that actually plays (base64 via the with-timestamps endpoint, since $.http.fetch is text-only), API-key handling with a sensitive userConfig field plus an env fallback, which events carry text worth speaking, playback limits, and how to stub ElevenLabs in `claude plugin test`. Use when a mod in this repo speaks, plays generated audio, or talks to the ElevenLabs API. Not for ElevenLabs Conversational AI agents (that's `elevenlabs-c...

  • 5 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 6, 2026
ai-agentsgotestingapi

Works with

  • claude code
  • terminal
  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 6, 2026

npx -y skills add leopiney/wolfbud-claude-mod --skill elevenlabs-mod --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Elevenlabs Mod?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Elevenlabs Mod
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/leopiney-elevenlabs-mod/badge)](https://www.skillsdirectory.com/skills/leopiney-elevenlabs-mod)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: elevenlabs-mod
description: Call ElevenLabs from inside a Claude Code mod - text-to-speech that actually plays (base64 via the with-timestamps endpoint, since $.http.fetch is text-only), API-key handling with a sensitive userConfig field plus an env fallback, which events carry text worth speaking, playback limits, and how to stub ElevenLabs in `claude plugin test`. Use when a mod in this repo speaks, plays generated audio, or talks to the ElevenLabs API. Not for ElevenLabs Conversational AI agents (that's `elevenlabs-conversational-agent`).
---

# ElevenLabs inside a mod

Read `mod-dev` (repo workflow) and `plugin-authoring` (the API) first. A complete, verified mod (`/say <text>`) lives in [example.md](example.md). It passes `claude plugin validate`, `tsc` and `claude plugin test` on Claude Code 2.1.287. Start from it.

## Hard constraints of the mod runtime

- **No SDK.** A mod imports nothing but its own files and `'claude-code'`, so `@elevenlabs/elevenlabs-js` is out. Call the REST API directly through `$.http.fetch`.
- **`$.http.fetch` is text in, text out.** `body` is a string, and the response is `{ status, ok, headers, text }`. A raw MP3 response would be mangled, which rules out the plain `/text-to-speech/{voice_id}` endpoint.
- **Use `POST /v1/text-to-speech/{voice_id}/with-timestamps`.** It returns JSON: `{ audio_base64, alignment, normalized_alignment }`. Pass `audio_base64` straight to `$.audio.play({ base64, mime: 'audio/mpeg' })`. `alignment` (`characters`, `character_start_times_seconds`, `character_end_times_seconds`) is a free bonus for karaoke-style highlighting in a pane.
- **Don't use `$.audio.play({ url })` for ElevenLabs.** The engine fetches the URL itself, and you can't attach the `xi-api-key` header.
- **Binary uploads** (speech-to-text, voice cloning: multipart) can't go through `$.http.fetch`. The fallback is `$.process.run(['curl', ...])`, which is CLI only and shows up as a `process` call in validate output. Prefer to avoid it.
- **Playback** goes through `afplay` on macOS. A Linux or Windows terminal plays nothing, and `$.audio.play` resolves anyway. Clips don't queue: two `play` calls overlap. Chain the promises yourself to serialize speech, and pass `signal` (an AbortController's) to stop a clip, e.g. when the user submits a new prompt.
- `$.audio.speak(text)` is the OS synthesizer (`say`), which is free and offline. Use it as a fallback when there's no API key or the request fails.

## API key

```json
"userConfig": {
  "api_key":  { "type": "string", "title": "ElevenLabs API key", "description": "...", "sensitive": true },
  "voice_id": { "type": "string", "title": "Voice ID", "description": "...", "default": "JBFqnCBsd6RMkjVDRZzb" }
}
```

- `sensitive: true` masks the input and keeps the key in secure storage, not `settings.json`. Values arrive as `register(on, options)`'s `options`.
- Don't mark it `required`, because a required field with no value fails the whole load. Fall back to `await $.env.get('ELEVENLABS_API_KEY')` (string literal, so validate lists it under `env reads:`). That fallback is also the easy path while developing with `--plugin-dir`.
- Never put the key in `$.ui.log`/`toast`/`status`, error text, or `$.store`.

## What to speak

- **`turn.complete`**: `e.answer` is the assistant's final visible text ('' when there is none). Skip when `e.agentId` is set (a subagent's turn), when `e.isAborted`, or when `e.reason !== 'answer'`. Always `return next(e)`.
- Strip markdown, code fences, file paths and URLs before sending, and cap the length. ElevenLabs bills per character, and a long answer read aloud is noise. Better patterns: speak a summary (`$.model.complete({ model, prompt })` yields a one-sentence version), or just the first paragraph.
- `prompt.submit`: a good place to abort the clip that's still playing.
- `tool.call` / `turn.start`: short cues ("running tests"). Pre-render fixed phrases once and cache them (base64 in `$.store`, or a file the mod ships played with `{ asset }`), so a cue costs no request.

## Not settled yet: verify before relying on it

- Whether awaiting a long `$.audio.play` inside `turn.complete` delays the next prompt. A hook's time budget excludes in-flight `$` calls, but the turn may still wait. If it does, push the text onto a queue and drain it from a `$.clock.after(0, ...)` timer started outside the dispatch. The `plugin-authoring` reference's "Work that outlives a dispatch" section describes the pattern.
- `audio.speak` is itself a hookable event. A mod could answer it so that every mod's `$.audio.speak` uses an ElevenLabs voice. Check the result shape in the types and write a test before building on this.

## Request defaults

- Header `xi-api-key: <key>`, `content-type: application/json`.
- Body `{ text, model_id }`. `eleven_flash_v2_5` is low-latency and cheap, good for interactive cues; `eleven_multilingual_v2` is the API default and higher quality. Optional: `voice_settings: { stability, similarity_boost, style, speed, use_speaker_boost }`, `language_code`, and `previous_text`/`next_text` for continuity across chunks.
- Query `output_format=mp3_44100_128` (the default; `mp3_22050_32` makes smaller payloads).
- On `!res.ok`: the body is JSON with a `detail`. Show the status code and a short reason; never the request.

## Testing it

Stub the network and the speaker beneath the plugin. Stubs answer `{ value }`:

```ts
mock.env(on, { ELEVENLABS_API_KEY: 'test-key' })
on('http.fetch', (_$, e) => ({ value: { status: 200, ok: true, headers: {}, text: JSON.stringify({ audio_base64: 'QUJD' }) } }))
on('audio.play', (_$, e) => ({ value: undefined }))   // e.clip.base64, e.clip.mime
```

Also cover the failure paths: no key (expect the hint text), `ok: false` (expect a readable error and no `audio.play`).

Files in this skill

  • SKILL.md5.7 KB
  • example.md3.9 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.