Skip to content

Latest commit

 

History

History
301 lines (208 loc) · 14.5 KB

tutorial.md

File metadata and controls

301 lines (208 loc) · 14.5 KB

I Built a Claude Code Plugin for ElevenLabs — Here's How and Why

By Kenny


ElevenLabs has one of the best voice AI APIs available. The documentation is solid, the SDKs cover Python and JavaScript, and the Conversational AI product is genuinely powerful. But when developers open a new project and type "how do I add a voice agent," Claude has no ElevenLabs-specific context. It hallucinates deprecated patterns, forgets required constructor arguments, and suggests synchronous TTS calls in real-time apps where latency matters.

A Claude Code plugin fixes that at the source. Instead of hoping developers find the right docs, the plugin brings the docs into the conversation — automatically, on every relevant prompt.

This is the plugin I'd ship on day one at ElevenLabs.


Why This Exists

The ElevenLabs Developer Relations job posting asks for someone who will "dive deep into where users are getting stuck" and "ship what they need to succeed." That's a specific philosophy: observe friction, remove it, repeat.

I've seen this pattern before. I built a Claude Code plugin for Wasp — 11 stars, 4 forks, 90 commits — because Wasp has a novel full-stack framework with patterns Claude gets wrong without guidance. The plugin brings Wasp's knowledge base into every Claude session, catches bad patterns before they're written, and steers developers toward the right abstractions.

ElevenLabs has the same problem. The Conversational AI SDK requires requires_auth and audio_interface in the Conversation() constructor. Skip either one and your agent crashes at runtime with an opaque error. The streaming TTS API is the right choice for real-time apps, but developers default to synchronous generate() calls and wonder why their UI feels sluggish. These aren't obscure edge cases — they're the first bugs developers hit.

The plugin addresses them systematically.


What the Plugin Does

/elevenlabs:init — Set up the knowledge base

The first command initializes ElevenLabs development context in your project. It creates .claude/elevenlabs/knowledge/ and copies a knowledge base covering SDK patterns, model selection, common pitfalls, and machine-readable anti-patterns. It then fetches the latest documentation index from elevenlabs.io/docs/llms.txt and appends it to the knowledge file. If the fetch fails, it warns you that the bundled snapshot is in use.

After setup, it appends an import to your CLAUDE.md:

# ElevenLabs Knowledge

ElevenLabs knowledge can be found at @.claude/elevenlabs/knowledge/general-elevenlabs-knowledge.md

Claude reads CLAUDE.md at the start of every session. That one line means every future conversation has ElevenLabs context without any extra prompting.

The command also scans your project: if it finds a package.json or requirements.txt, it suggests /elevenlabs:add-tts. If it detects voice agent code, it suggests /elevenlabs:build-agent. If it finds an ELEVENLABS_AGENT_ID in an env file, it suggests /elevenlabs:test-agent. That's not magic — it's just knowing what developers are likely building given what they've already set up.

/elevenlabs:build-agent — Scaffold a voice agent

This skill scaffolds a Conversational AI voice agent using correct SDK patterns for your language. The critical detail: ElevenLabs' Python Conversation() constructor requires both requires_auth and audio_interface as keyword arguments. A naive scaffold omits them. This one doesn't.

For Python:

from elevenlabs.client import ElevenLabs
from elevenlabs.conversational_ai.conversation import Conversation
from elevenlabs.conversational_ai.default_audio_interface import DefaultAudioInterface

client = ElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])

conversation = Conversation(
    client=client,
    agent_id=os.environ["ELEVENLABS_AGENT_ID"],
    requires_auth=False,
    audio_interface=DefaultAudioInterface(),
    callback_agent_response=lambda r: print(f"Agent: {r}"),
    callback_user_transcript=lambda t: print(f"User: {t}"),
)

conversation.start_session()

For React, the skill scaffolds a component using @11labs/react's useConversation hook with proper session lifecycle handling.

/elevenlabs:add-tts — Add text-to-speech

This skill adds TTS to an existing application. It detects your framework (React, Node.js, Python) and scaffolds accordingly. It also makes model selection explicit, because the right choice depends on your use case:

Model Use Case Latency
eleven_turbo_v2_5 Real-time UI narration Low
eleven_flash_v2_5 Low-latency applications Very Low
eleven_multilingual_v2 High-quality content, 29 languages Medium

The skill defaults to streaming (convert_as_stream / convertAsStream) for real-time applications. Synchronous calls are available but flagged as appropriate only for batch jobs and content pipelines where latency doesn't matter.

/elevenlabs:test-agent — Test via the Simulate Conversation API

Before deploying a voice agent, you want to verify it responds correctly to scripted inputs. This skill uses ElevenLabs' Simulate Conversation API to run test scenarios against your agent without requiring audio hardware. You can define test turns, expected responses, and pass/fail criteria. It's particularly useful in CI/CD pipelines where you can't run a real audio session.

/elevenlabs:expert-advice — Review your code

This command audits all ElevenLabs code in your project against a structured checklist:

  • API usage patterns (streaming vs. batch, correct endpoints)
  • Authentication and key management
  • Error handling and retry logic
  • Voice agent lifecycle (session start/end, WebSocket cleanup)
  • ClientTools registration
  • Audio format and quality settings
  • Rate limiting awareness

It fetches the latest docs before running the review. If the fetch fails, it uses the local knowledge base and warns you. Results are organized by severity, and it offers to apply fixes directly.

Hooks — Proactive guardrails

Three hooks run automatically without any user action.

SessionStart runs check-elevenlabs-init.js when Claude opens a session. If it detects ElevenLabs code in the project but the plugin hasn't been initialized, it tells Claude to prompt you to run /elevenlabs:init.

UserPromptSubmit runs enforce-knowledge.js on every prompt. It checks for ElevenLabs-related keywords and, when found, adds the knowledge base path to Claude's context. It also maps compound keywords to relevant skills — if you type "voice agent," it surfaces /elevenlabs:build-agent. If you type "text to speech," it surfaces /elevenlabs:add-tts.

PreToolUse runs check-api-patterns.js before every Edit or Write operation. If the file being written contains ElevenLabs code, the hook scans it against the anti-patterns registry and injects warnings into Claude's context before the write completes.


How I Built It

The architecture follows the same pattern I proved with the Wasp plugin: a knowledge base file shared between hooks and commands, live doc fetching with offline fallback, and hooks that catch problems early rather than documenting them after the fact.

The hooks.json structure

Claude Code's plugin system uses a hooks.json file to wire events to scripts:

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/check-elevenlabs-init.js\""
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/enforce-knowledge.js\""
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/check-api-patterns.js\""
          }
        ]
      }
    ]
  }
}

${CLAUDE_PLUGIN_ROOT} resolves to the plugin's installation directory at runtime. The matcher on PreToolUse limits the hook to Edit and Write tool calls — there's no reason to scan file reads or bash commands.

Compound keyword detection

The UserPromptSubmit hook uses compound keyword matching to avoid false positives. A single word like "voice" is too broad. Instead, the hook checks for multi-word phrases:

const patterns = [
  {
    keywords: ['voice agent', 'voice assistant', 'phone bot', 'chatbot'],
    suggestion: 'Use `/elevenlabs:build-agent` to scaffold a conversational voice agent.',
  },
  {
    keywords: ['text to speech', 'read aloud', 'narration', 'tts integration'],
    suggestion: 'Use `/elevenlabs:add-tts` to add text-to-speech to your app.',
  },
];

Each pattern maps keywords to a specific skill. The hook only fires when it finds a match — it calls process.exit(0) silently on no-match to avoid adding noise to every prompt.

Dynamic pattern reading in PreToolUse

The PreToolUse hook doesn't hardcode anti-patterns. It reads them from the knowledge base file at runtime:

const startMarker = '<!-- HOOK_PATTERNS_START -->';
const endMarker = '<!-- HOOK_PATTERNS_END -->';
const startIdx = knowledgeContent.indexOf(startMarker);
const endIdx = knowledgeContent.indexOf(endMarker);

const jsonBlock = knowledgeContent.slice(startIdx + startMarker.length, endIdx);
const jsonMatch = jsonBlock.match(/```json\s*([\s\S]*?)```/);
const { patterns } = JSON.parse(jsonMatch[1]);

This means updating the anti-patterns list requires only editing the knowledge base file — not touching the hook code. When you run /elevenlabs:update-docs, the knowledge base updates. The hook picks up the new patterns on the next write.


The Knowledge Base Approach

The heart of the plugin is general-elevenlabs-knowledge.md. It contains SDK patterns, model selection guidance, error handling examples, project structure recommendations, and the machine-readable anti-patterns block.

How llms.txt keeps it current

ElevenLabs publishes a documentation index at elevenlabs.io/docs/llms.txt. This is a machine-readable list of doc URLs following the llms.txt standard. The update-docs-index.js script fetches this file and appends a structured documentation index to the knowledge base. Claude can then fetch individual doc pages by URL when it needs current API reference.

The flow:

  1. /elevenlabs:init runs the update script on first setup
  2. /elevenlabs:update-docs re-runs it when docs have changed
  3. Commands that need current docs (like expert-advice) fetch from the index before running

If the fetch fails, the knowledge base still works — it falls back to the bundled snapshot and warns the user. Offline development keeps working.

The anti-patterns block

The anti-patterns block is a JSON structure embedded in the Markdown file between HTML comment markers. This lets one file serve two purposes: human-readable documentation and machine-readable data for hooks.

{
  "patterns": [
    {
      "id": "hardcoded-api-key",
      "regex": "ELEVENLABS_API_KEY\\s*=\\s*['\"][a-zA-Z0-9]",
      "message": "Hardcoded API key detected — use environment variables (process.env.ELEVENLABS_API_KEY or os.environ)",
      "severity": "critical"
    },
    {
      "id": "conversation-missing-auth",
      "regex": "Conversation\\((?!.*requires_auth)",
      "message": "Missing requires_auth in Conversation init — this is a required kwarg",
      "severity": "high"
    },
    {
      "id": "conversation-missing-audio",
      "regex": "Conversation\\((?!.*audio_interface)",
      "message": "Missing audio_interface in Conversation init — this is a required kwarg",
      "severity": "high"
    },
    {
      "id": "sync-tts-call",
      "regex": "generate\\(.*text.*\\)(?!.*stream)",
      "message": "Consider using streaming TTS (convert_as_stream / convertAsStream) for better latency in real-time applications",
      "severity": "medium"
    }
  ]
}

These four patterns cover the most common bugs I've seen when developers start with ElevenLabs. The first is a security issue. The second and third are runtime crashes. The fourth is a performance issue that doesn't crash but degrades user experience.

The expert-advice command reads the same block when running its code review. That means the hook and the command always check against the same set of rules — no divergence, no duplication.


What I'd Do Next

Phase 2 skills

Three skills are already scoped:

  • /elevenlabs:add-voice — voice selection, cloning workflow, and library management
  • /elevenlabs:add-dubbing — dubbing and audio localization for existing content
  • /elevenlabs:start-dev-server — local development server with live reload for agent iteration

Longer-term ideas

Analytics integration. The Conversational AI API returns conversation metadata including transcript, latency, and turn counts. A skill that parses this data and surfaces patterns — "your agent takes an average of 2.3 seconds to respond in turn 3" — would help developers identify where their agent needs tuning.

Conversation replay. Record a real user session, replay it against a new agent version, compare responses. This is the foundation of regression testing for voice agents.

A/B voice testing. Run the same conversation scenario against two different voices or models, generate a side-by-side comparison. Useful for teams choosing between eleven_turbo_v2_5 and eleven_flash_v2_5 for a specific use case.

How this scales

The architecture isn't ElevenLabs-specific. The knowledge base pattern, live doc fetching via llms.txt, marker-delimited machine-readable data, and the three-hook structure all transfer directly to any AI platform with a Claude Code plugin. I've now built this architecture twice — once for Wasp, once for ElevenLabs — and the second iteration was faster and more robust because the patterns were proven.


Try It

The plugin is available on npm:

npm install -g elevenlabs-claude-plugin

Then in any project:

/elevenlabs:init

The source is on GitHub: github.com/your-username/elevenlabs-claude-plugin

If you're building with ElevenLabs and Claude Code, this is the fastest way to get correct SDK patterns, live documentation access, and anti-pattern guardrails without leaving your editor.


Kenny is a developer relations engineer specializing in AI developer tools. He's applying for the ElevenLabs Developer Relations Engineer role.