VibeCody Plugin Development Guide

Complete reference for building VibeCody plugins, WASM extensions, MCP integrations, skills, and hooks. This document is designed for both human developers and AI coding assistants.


Table of Contents

  1. Architecture Overview
  2. Plugin System
  3. Skills
  4. Hooks
  5. WASM Extensions
  6. MCP Server Integration
  7. ACP Protocol
  8. HTTP Daemon API
  9. AI Provider Trait
  10. Tool System
  11. Agent Loop
  12. Configuration Reference
  13. Examples

Architecture Overview

VibeCody has a five-layer extensibility architecture:

Layer Type Runtime Location
Skills Markdown with YAML front-matter Loaded at startup, matched by trigger keywords ~/.vibecli/skills/ or <workspace>/.vibecli/skills/
Hooks Shell scripts, LLM prompts, or HTTP webhooks Fired on agent events (pre/post tool use, file save, etc.) Configured in ~/.vibecli/config.toml
Plugins Bundles of skills + hooks + commands Installed to ~/.vibecli/plugins/<name>/ Registry at https://registry.vibecody.dev/api/v1
WASM Extensions Sandboxed WebAssembly modules Loaded by VibeUI (Tauri desktop app) ~/.vibeui/extensions/
MCP/ACP JSON-RPC over stdio or HTTP Bidirectional tool integration Configured in config.toml or via --mcp-server flag

Monorepo Structure

vibecody/
  vibecli/vibecli-cli/src/     # CLI binary (Rust)
    main.rs                     # Entry point, CLI args, command dispatch
    plugin.rs                   # Plugin loader & manifest parsing
    plugin_sdk.rs               # Plugin SDK types, scaffolding
    plugin_registry.rs          # Registry client, search, publish
    plugin_lifecycle.rs         # Install, enable, disable, update
    tool_executor.rs            # Tool call execution
    config.rs                   # Configuration at ~/.vibecli/config.toml
    serve.rs                    # HTTP daemon (REST + SSE)
    mcp_server.rs               # MCP server (JSON-RPC over stdio)
    acp.rs                      # Agent Communication Protocol
  vibecli/vibecli-cli/skills/   # 526 built-in skill files
  vibeui/crates/
    vibe-ai/src/
      provider.rs               # AIProvider trait
      agent.rs                  # AgentLoop, AgentContext, AgentEvent
      tools.rs                  # ToolCall, ToolResult, parsing
      hooks.rs                  # HookRunner, HookEvent, HookDecision
      skills.rs                 # SkillLoader, Skill, SkillWatcher
      mcp.rs                    # MCP client
    vibe-core/src/              # Text buffer, filesystem, git, search, index
    vibe-extensions/src/
      lib.rs                    # Extension loader (wasmtime)
      api.rs                    # Host function constants
      manifest.rs               # ExtensionManifest, Permission, Registry
  vibeui/src/                   # React/TypeScript frontend (Tauri 2)

Plugin System

Plugin Directory Layout

~/.vibecli/plugins/my-plugin/
  plugin.toml          # Required manifest
  skills/              # Auto-activated skill files (.md)
    my-skill.md
  hooks/               # Event-triggered scripts
    pre-tool.sh
    post-write.sh
  commands/            # REPL commands (/my-plugin-cmd)
    lint.sh
  README.md            # Optional documentation

Plugin Manifest (plugin.toml)

Basic Manifest

name = "my-plugin"
version = "1.0.0"
description = "What this plugin does"
author = "Your Name"

[[hooks]]
event = "PostToolUse"
tools = ["write_file", "apply_patch"]
command = "hooks/post-write.sh"

Full Manifest (V2)

name = "my-plugin"
version = "1.0.0"
display_name = "My Plugin"
description = "A detailed description of the plugin"
author = "Your Name"
license = "MIT"
repository = "https://github.com/user/vibecody-my-plugin"
homepage = "https://example.com"
kind = "connector"       # connector | adapter | optimizer | theme | skillpack | workflow | extension
min_vibecli_version = "0.1.0"
max_vibecli_version = "2.0.0"
keywords = ["jira", "project-management", "issues"]
icon = "icon.png"
platforms = ["macos", "linux", "windows"]

# Capabilities the plugin requires
capabilities = ["FileRead", "FileWrite", "NetworkAccess"]

# Dependencies on other plugins
[[dependencies]]
name = "vibecody-core-utils"
version = ">=1.0.0"

# Hook definitions
[[hooks]]
event = "PostToolUse"
tools = ["write_file"]
handler = "hooks/post-write.sh"
priority = 100           # Lower runs first
async_exec = false       # true = non-blocking

# Command definitions
[[commands]]
name = "lint"
description = "Run linter on changed files"
handler = "commands/lint.sh"

[[commands.args]]
name = "fix"
description = "Auto-fix issues"
required = false
default = "false"

# Plugin-specific settings
[[settings]]
key = "api_url"
description = "API endpoint URL"
type = "string"
required = true

[[settings]]
key = "api_token"
description = "API authentication token"
type = "secret"          # string | number | boolean | secret | filepath | enum
required = true

[[settings]]
key = "severity"
description = "Minimum severity to report"
type = "enum"
values = ["low", "medium", "high", "critical"]
default = "medium"

Plugin Kind

Kind Purpose Example
Connector Integrate external services (Jira, Linear, Notion) vibecody-jira
Adapter Add AI providers, gateways, container runtimes vibecody-terraform
Optimizer Linters, formatters, refactoring tools vibecody-prettier
Theme UI themes and color schemes vibecody-dracula-theme
SkillPack Bundles of skill markdown files vibecody-devops-pack
Workflow Pre-built agent workflow templates vibecody-code-review
Extension WASM extensions for VibeUI vibecody-minimap

Plugin Capabilities

Capability Description Dangerous?
FileRead Read files in workspace No
FileWrite Write/modify files Yes
NetworkAccess Make HTTP requests Yes
ProcessExec Execute shell commands Yes
EnvRead Read environment variables No
Notification Show notifications No
Clipboard Access clipboard No
GitAccess Run git commands No
DatabaseAccess Access databases Yes
HttpServer Start HTTP servers No
WebSocket Open WebSocket connections No

Plugins requesting dangerous capabilities must provide descriptions of at least 20 characters.

Plugin CLI Commands

# Create a new plugin from template
vibecli --plugin create my-plugin --kind connector

# Install from registry or Git repo
vibecli --plugin install vibecody-jira
vibecli --plugin install https://github.com/user/vibecody-my-plugin

# Lifecycle management
vibecli --plugin enable my-plugin
vibecli --plugin disable my-plugin
vibecli --plugin uninstall my-plugin
vibecli --plugin update my-plugin
vibecli --plugin update              # Update all

# Discovery
vibecli --plugin list
vibecli --plugin info my-plugin
vibecli --plugin search "jira"

# Development
vibecli --plugin dev --watch         # Hot-reload during development

# Publishing
vibecli --plugin publish ./my-plugin

Plugin Lifecycle States

Installed → Enabled ↔ Disabled
              ↓
           Outdated (newer version available)
              ↓
           DevMode (locally linked)
              ↓
           Errored (load failure)

Plugin State Storage

Stored in ~/.vibecli/plugin-state.json:

{
  "version": "1",
  "plugins": [
    {
      "name": "vibecody-jira",
      "version": "1.2.0",
      "state": "Enabled",
      "install_dir": "/Users/you/.vibecli/plugins/vibecody-jira",
      "installed_at": "2026-03-01T10:00:00Z",
      "config": {
        "api_url": "https://company.atlassian.net",
        "api_token": "***"
      },
      "checksum": "sha256:abc123..."
    }
  ]
}

Skills

Skills are context-aware capability bundles injected into the agent’s system prompt when trigger keywords match the user’s input.

Skill File Format

---
triggers: ["docker", "container", "dockerfile", "docker-compose"]
tools_allowed: ["read_file", "write_file", "bash"]
category: devops
---

# Docker Best Practices

When working with Docker:

1. **Use multi-stage builds** to reduce image size...
2. **Never run as root** — add a non-root user...
3. **Pin base image versions** — use `node:20-alpine` not `node:latest`...

Skill Front-Matter Fields

Field Type Required Description
triggers string[] Yes Keywords that activate this skill (case-insensitive substring match)
tools_allowed string[] No Restrict which tools the agent can use (empty = no restriction)
category string No Grouping category for organization
name string No Override the display name (defaults to filename)
description string No Short description
version string No Semantic version
requires.bins string[] No Required CLI binaries (e.g., cargo, docker)
requires.env string[] No Required environment variables
requires.os string[] No OS filter: macos, linux, windows
install.brew string No Homebrew package to auto-install
install.npm string No npm package to auto-install
install.cargo string No Cargo package to auto-install
install.pip string No pip package to auto-install
config.<KEY> string No Per-skill configuration key-value
webhook_trigger string No URL path that triggers this skill (e.g., /webhook/deploy)

Skill Loading Priority

  1. <workspace>/.vibecli/skills/ (project-specific)
  2. ~/.vibecli/skills/ (user-global)
  3. ~/.vibecli/plugins/*/skills/ (from installed plugins)
  4. vibecli/vibecli-cli/skills/ (built-in, 526 skills)

Later sources do NOT override earlier ones. All matching skills are injected.

Skill Matching

Skills match when any trigger keyword appears as a substring in the user’s message (case-insensitive). Multiple skills can match simultaneously.

// Pseudo-code for matching
fn matches(skill: &Skill, user_message: &str) -> bool {
    let lower = user_message.to_lowercase();
    skill.triggers.iter().any(|t| lower.contains(&t.to_lowercase()))
}

Hooks

Hooks intercept agent events and can allow, block, or inject context.

Hook Configuration (~/.vibecli/config.toml)

# Shell command hook — runs on every file write
[[hooks]]
event = "PostToolUse"
tools = ["write_file"]
handler = { command = "sh .vibecli/hooks/format.sh" }
async = false

# LLM hook — AI-powered safety check
[[hooks]]
event = "PreToolUse"
tools = ["bash"]
handler = { llm = "Check if this bash command is safe. Block if it could delete important data." }

# HTTP webhook — external service integration
[[hooks]]
event = "FileSaved"
paths = ["**/*.rs"]
handler = { url = "https://linter.example.com/check", method = "POST", timeout_ms = 5000 }
async = true

Hook Events

Event Description Blockable?
SessionStart Agent session begins No
UserPromptSubmit User sends a message Yes
PreToolUse Before a tool call executes Yes
PostToolUse After a tool call completes No (can inject context)
Stop Agent stops No
TaskCompleted Agent completes task No
SubagentStart Sub-agent spawned No
FileSaved File written to disk No (can inject context)
FileCreated New file created No
FileDeleted File deleted No

Hook Handlers

Shell Command Handler

Receives JSON event on stdin. Protocol:

Exit Code Result
0 Allow
2 Block (stderr = reason)
Other Allow (treated as error)

Or write JSON to stdout:

{"allow": false, "reason": "Blocked: command deletes production data"}
{"context": "Note: the previous write triggered a lint warning on line 42"}

Example hook script:

#!/bin/bash
# .vibecli/hooks/format.sh
# Auto-format Rust files after write

EVENT=$(cat)  # Read JSON from stdin
FILE=$(echo "$EVENT" | jq -r '.call.path // empty')

if [[ "$FILE" == *.rs ]]; then
    rustfmt "$FILE" 2>/dev/null
fi

exit 0  # Always allow

LLM Handler

The prompt is sent to the configured LLM provider with the event context. Expected response:

{"ok": true}

or

{"ok": false, "reason": "This command would delete the database"}

HTTP Handler

POST the event JSON to the URL. Expected response:

{
  "decision": "allow",
  "reason": "",
  "context": ""
}

decision can be "allow", "block", or "inject".

Hook Filtering

[[hooks]]
event = "PostToolUse"
tools = ["write_file", "apply_patch"]   # Only file-write tools
paths = ["src/**/*.rs", "tests/**"]     # Only Rust source files
handler = { command = "cargo clippy" }

WASM Extensions

Sandboxed WebAssembly modules that extend VibeUI (the desktop app).

Extension Structure

~/.vibeui/extensions/my-extension/
  extension.json         # Manifest
  extension.wasm         # Compiled WASM module

Extension Manifest (extension.json)

{
  "name": "my-extension",
  "version": "1.0.0",
  "display_name": "My Extension",
  "author": "Your Name",
  "description": "What this extension does",
  "permissions": ["FileRead", "Notify"],
  "min_host_version": "0.1.0",
  "wasm_path": "extension.wasm"
}

Permissions

Permission Auto-granted? Description
FileRead Yes Read files in workspace
FileWrite No (dangerous) Write/modify files
Network No (dangerous) Make network requests
ProcessExec No (dangerous) Execute processes
EnvRead No Read environment variables
Notify Yes Show notifications
Clipboard No Access clipboard

WASM Host API

Extensions are loaded by wasmtime. The host module is "vibeui_host".

Host Functions (imported by WASM)

// Log a message
fn log(ptr: i32, len: i32)

// Read a file; returns bytes written to out buffer, or -1 on error
fn read_file(path_ptr: i32, path_len: i32, out_ptr: i32, out_cap: i32) -> i32

// Write a file; returns 0 on success, -1 on error
fn write_file(path_ptr: i32, path_len: i32, data_ptr: i32, data_len: i32) -> i32

// Show a notification
fn notify(ptr: i32, len: i32)

Extension Exports (implemented by WASM)

// Required: memory allocator for string passing
fn alloc(len: i32) -> i32

// Required: linear memory
memory: WebAssembly.Memory

// Optional: called once on load
fn init() -> i32

// Optional: called when a file is saved
fn on_file_save(ptr: i32, len: i32)

// Optional: called when text changes in editor
fn on_text_change(ptr: i32, len: i32)

Memory Limits

Writing an Extension in Rust

// lib.rs — compiled to wasm32-wasi target

extern "C" {
    fn log(ptr: *const u8, len: usize);
    fn notify(ptr: *const u8, len: usize);
    fn read_file(path_ptr: *const u8, path_len: usize, out_ptr: *mut u8, out_cap: usize) -> i32;
}

#[no_mangle]
pub extern "C" fn alloc(len: usize) -> *mut u8 {
    let mut buf = Vec::with_capacity(len);
    let ptr = buf.as_mut_ptr();
    std::mem::forget(buf);
    ptr
}

#[no_mangle]
pub extern "C" fn init() -> i32 {
    let msg = "Extension loaded!";
    unsafe { log(msg.as_ptr(), msg.len()); }
    0
}

#[no_mangle]
pub extern "C" fn on_file_save(ptr: *const u8, len: usize) {
    let path = unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(ptr, len)) };
    if path.ends_with(".rs") {
        let msg = format!("Rust file saved: {}", path);
        unsafe { notify(msg.as_ptr(), msg.len()); }
    }
}

Compile: cargo build --target wasm32-wasi --release


MCP Server Integration

VibeCLI can run as an MCP (Model Context Protocol) server, exposing its tools to Claude Desktop and other MCP clients.

Starting the MCP Server

vibecli --mcp-server

Claude Desktop Configuration

Add to ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "vibecli": {
      "command": "vibecli",
      "args": ["--mcp-server"],
      "cwd": "/path/to/your/project"
    }
  }
}

Exposed MCP Tools

Tool Description Parameters
read_file Read a file from the workspace path: string
write_file Write content to a file path: string, content: string
list_directory List directory contents path: string
bash Execute a shell command command: string
search_files Full-text search across files query: string, glob?: string
agent_run Spawn an agent for a task task: string

Protocol

Connecting to External MCP Servers

VibeCLI can also act as an MCP client, connecting to external MCP servers:

# ~/.vibecli/config.toml

[[mcp_servers]]
name = "github"
command = "npx"
args = ["@modelcontextprotocol/server-github", "--token", "$GITHUB_TOKEN"]

[[mcp_servers]]
name = "filesystem"
command = "npx"
args = ["@modelcontextprotocol/server-filesystem", "/path/to/allowed"]

External MCP tools become available to the agent as additional tool calls.


ACP Protocol

The Agent Communication Protocol (ACP) enables external IDEs and tools to submit tasks to VibeCLI’s HTTP daemon.

Endpoints

Method Path Description
GET /acp/v1/capabilities List agent capabilities
POST /acp/v1/tasks Submit a task
GET /acp/v1/tasks/:id Get task status
GET /acp/v1/tasks/:id/events SSE stream of task events

Capabilities Response

{
  "protocol_version": "1.0",
  "agent_name": "vibecli",
  "agent_version": "0.1.0",
  "supported_tools": ["read_file", "write_file", "bash", "search_files", "list_directory", "web_search", "fetch_url", "spawn_agent"],
  "supported_models": ["claude-sonnet-4-20250514"],
  "features": ["streaming", "vision", "hooks", "mcp"]
}

Submit Task

curl -X POST http://localhost:7878/acp/v1/tasks \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "task": "Add error handling to the login function",
    "context": {
      "workspace_root": "/path/to/project",
      "files": [
        {"path": "src/auth.rs", "content": "...", "selection": {"start_line": 10, "start_col": 0, "end_line": 25, "end_col": 0}}
      ]
    },
    "model": "claude-sonnet-4-20250514",
    "approval_policy": "auto_edit"
  }'

Task Status Response

{
  "id": "task-abc123",
  "status": "complete",
  "summary": "Added Result return type and ? propagation to login()",
  "files_modified": ["src/auth.rs"],
  "steps_completed": 3
}

HTTP Daemon API

Start the daemon:

vibecli serve --port 7878 --provider claude

All endpoints require Authorization: Bearer <token> (token from config or VIBECLI_API_TOKEN).

Endpoints

Method Path Description
GET /health Health check (returns version, provider, status)
POST /chat Single-turn chat completion
POST /chat/stream Streaming chat (SSE)
POST /agent Start background agent task
GET /stream/:session_id SSE stream of agent events
GET /jobs List all job records
GET /jobs/:id Get single job record
POST /jobs/:id/cancel Cancel a running job
GET /sessions HTML index of sessions
GET /sessions.json JSON list of sessions
GET /view/:id HTML view of session transcript
GET /share/:id Shareable readonly view

Chat Request

curl -X POST http://localhost:7878/chat \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"messages": [{"role": "user", "content": "Explain async in Rust"}]}'

Response:

{"content": "In Rust, async/await works through..."}

Agent Request

curl -X POST http://localhost:7878/agent \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"task": "Fix the failing test in tests/auth.rs", "approval": "full_auto"}'

Response:

{"session_id": "abc123"}

SSE Event Stream

curl -N http://localhost:7878/stream/abc123 \
  -H "Authorization: Bearer $TOKEN"

Event types:

data: {"kind": "chunk", "content": "Let me read the test file..."}
data: {"kind": "step", "step_num": 1, "tool_name": "read_file", "success": true}
data: {"kind": "step", "step_num": 2, "tool_name": "write_file", "success": true}
data: {"kind": "complete", "content": "Fixed the assertion on line 42"}

AI Provider Trait

All 17 AI providers implement the AIProvider trait:

#[async_trait]
pub trait AIProvider: Send + Sync {
    fn name(&self) -> &str;
    async fn is_available(&self) -> bool;
    async fn complete(&self, context: &CodeContext) -> Result<CompletionResponse>;
    async fn stream_complete(&self, context: &CodeContext) -> Result<CompletionStream>;
    async fn chat(&self, messages: &[Message], context: Option<String>) -> Result<String>;
    async fn stream_chat(&self, messages: &[Message]) -> Result<CompletionStream>;
    async fn chat_response(&self, messages: &[Message], context: Option<String>) -> Result<CompletionResponse>;
    async fn chat_with_images(
        &self, messages: &[Message], images: &[ImageAttachment], context: Option<String>
    ) -> Result<String>;
    fn supports_vision(&self) -> bool;
}

Key Types

pub struct Message {
    pub role: MessageRole,     // System, User, Assistant
    pub content: String,
}

pub struct CodeContext {
    pub language: String,
    pub file_path: Option<String>,
    pub prefix: String,        // Code before cursor
    pub suffix: String,        // Code after cursor
    pub additional_context: Vec<String>,
}

pub struct ProviderConfig {
    pub provider_type: String, // "claude", "openai", "ollama", etc.
    pub api_key: Option<String>,
    pub api_url: Option<String>,
    pub model: String,
    pub max_tokens: Option<usize>,
    pub temperature: Option<f32>,
    pub api_key_helper: Option<String>,
    pub thinking_budget_tokens: Option<u32>,
}

pub struct CompletionResponse {
    pub text: String,
    pub model: String,
    pub usage: Option<TokenUsage>,
}

pub struct TokenUsage {
    pub prompt_tokens: u32,
    pub completion_tokens: u32,
}

pub struct ImageAttachment {
    pub base64: String,
    pub media_type: String,    // "image/png", "image/jpeg", etc.
}

Supported Providers (17)

# Provider Config Key Models
1 Ollama ollama Any local model
2 Anthropic Claude claude claude-sonnet-4, claude-opus-4
3 OpenAI openai gpt-4o, gpt-4o-mini, o1
4 Google Gemini gemini gemini-2.0-flash, gemini-pro
5 xAI Grok grok grok-2, grok-3
6 Groq groq llama-3.3-70b, mixtral
7 OpenRouter openrouter 300+ models
8 Azure OpenAI azure_openai Deployed models
9 AWS Bedrock bedrock Claude, Titan, Llama
10 GitHub Copilot copilot Copilot models
11 Mistral mistral mistral-large, codestral
12 Cerebras cerebras Fast inference
13 DeepSeek deepseek deepseek-coder
14 Zhipu zhipu GLM-4
15 Vercel AI vercel_ai Unified proxy
16 LocalEdit local_edit Inline edits
17 Failover failover Auto-failover chain

Tool System

The agent uses XML-based tool calling that works with all providers.

Available Tools

Tool XML Tag Description
read_file <tool_call><name>read_file</name><path>...</path></tool_call> Read file contents
write_file <tool_call><name>write_file</name><path>...</path><content>...</content></tool_call> Write/create file
apply_patch <tool_call><name>apply_patch</name><path>...</path><patch>...</patch></tool_call> Apply unified diff
bash <tool_call><name>bash</name><command>...</command></tool_call> Execute shell command
search_files <tool_call><name>search_files</name><query>...</query></tool_call> Full-text search
list_directory <tool_call><name>list_directory</name><path>...</path></tool_call> List directory
web_search <tool_call><name>web_search</name><query>...</query></tool_call> Web search
fetch_url <tool_call><name>fetch_url</name><url>...</url></tool_call> Fetch URL content
task_complete <tool_call><name>task_complete</name><summary>...</summary></tool_call> Signal completion
spawn_agent <tool_call><name>spawn_agent</name><task>...</task></tool_call> Spawn sub-agent

Tool Result Format

pub struct ToolResult {
    pub tool_name: String,
    pub output: String,      // Truncated at 8000 chars
    pub success: bool,
    pub truncated: bool,
}

Destructive Tools

These tools require approval under Suggest and AutoEdit policies:


Agent Loop

Approval Policies

Policy Behavior
Suggest Show each tool call, wait for user approval (y/n/a)
AutoEdit Auto-approve file operations, prompt for bash
FullAuto Execute all tool calls without prompting

Agent Events

pub enum AgentEvent {
    StreamChunk(String),           // LLM output token
    ToolCallPending { call, tx },  // Awaiting approval
    ToolCallExecuted(AgentStep),   // Tool completed
    Complete(String),              // Task finished
    Error(String),                 // Fatal error
    CircuitBreak { state, reason },// Health check triggered
}

Circuit Breaker

The agent monitors its own health:

State Trigger Action
Progress Normal operation Continue
Stalled No file changes for 5 steps Suggest new approach
Spinning Same error 3 times Suggest alternative
Degraded Output volume dropped 50%+ Warn user
Blocked External blocker Report and stop

Agent Context

pub struct AgentContext {
    pub workspace_root: PathBuf,
    pub open_files: Vec<String>,
    pub git_branch: Option<String>,
    pub git_diff_summary: Option<String>,
    pub flow_context: Option<String>,
    pub approved_plan: Option<String>,
    pub extra_skill_dirs: Vec<PathBuf>,
    pub parent_session_id: Option<String>,
    pub depth: u32,                       // Nesting depth (0 = root)
    pub team_bus: Option<TeamMessageBus>, // Inter-agent messaging
}

Configuration Reference

Main Config (~/.vibecli/config.toml)

# Provider selection
[ollama]
model = "llama3.2"
api_url = "http://localhost:11434"

[claude]
api_key = "sk-..."
model = "claude-sonnet-4-20250514"
api_url = "https://api.anthropic.com"       # Configurable
thinking_budget_tokens = 10000               # Extended thinking

[openai]
api_key = "sk-..."
model = "gpt-4o"
api_url = "https://api.openai.com/v1"       # Configurable

# Failover chain — auto-switch on provider failure
[failover]
providers = ["claude", "openai", "ollama"]

# Index configuration
[index]
embedding_provider = "ollama"
embedding_model = "nomic-embed-text"

# Hook definitions
[[hooks]]
event = "PostToolUse"
tools = ["write_file"]
handler = { command = "sh .vibecli/hooks/format.sh" }

# MCP server connections
[[mcp_servers]]
name = "github"
command = "npx"
args = ["@modelcontextprotocol/server-github"]

# UI preferences
[ui]
theme = "dark"

# Safety settings
[safety]
max_file_size = 1048576
blocked_commands = ["rm -rf /", "mkfs", "dd if="]

# Tool configuration
[tools]
search_engine = "duckduckgo"    # duckduckgo | tavily | brave
tavily_api_key = ""
brave_api_key = ""

# Memory (auto-facts)
[memory]
enabled = true
max_facts = 100

# Shell environment policy
[tools.env]
inherit = "core"                 # all | core | none
include = ["PATH", "HOME", "LANG", "NODE_*"]
exclude = ["AWS_SECRET*", "*_TOKEN"]

[tools.env.set]
EDITOR = "vim"

# Routing (use different providers for planning vs execution)
[routing]
planner_provider = "claude"
executor_provider = "ollama"

# Sandbox settings
[sandbox]
enabled = false
runtime = "docker"               # docker | podman | opensandbox
image = "ubuntu:22.04"

Environment Variables

Variable Description
ANTHROPIC_API_KEY Anthropic Claude API key
OPENAI_API_KEY OpenAI API key
GEMINI_API_KEY Google Gemini API key
GROK_API_KEY xAI Grok API key
GROQ_API_KEY Groq API key
OPENROUTER_API_KEY OpenRouter API key
MISTRAL_API_KEY Mistral API key
DEEPSEEK_API_KEY DeepSeek API key
VIBECLI_API_TOKEN HTTP daemon auth token
GITHUB_TOKEN GitHub/Copilot token

Examples

Example 1: Jira Connector Plugin

vibecody-jira/
  plugin.toml
  skills/
    jira-workflow.md
  hooks/
    sync-on-complete.sh
  commands/
    create-ticket.sh
    list-tickets.sh

plugin.toml:

name = "vibecody-jira"
version = "1.2.0"
display_name = "Jira Integration"
description = "Sync VibeCLI tasks with Jira issues"
author = "VibeCody Team"
license = "MIT"
kind = "connector"
capabilities = ["NetworkAccess", "FileRead"]
keywords = ["jira", "project-management", "issues", "agile"]

[[settings]]
key = "jira_url"
description = "Jira instance URL"
type = "string"
required = true

[[settings]]
key = "jira_token"
description = "Jira API token"
type = "secret"
required = true

[[hooks]]
event = "TaskCompleted"
handler = "hooks/sync-on-complete.sh"

[[commands]]
name = "create-ticket"
description = "Create a Jira ticket from the current task"
handler = "commands/create-ticket.sh"

[[commands.args]]
name = "project"
description = "Jira project key"
required = true

[[commands.args]]
name = "type"
description = "Issue type"
required = false
default = "Task"

skills/jira-workflow.md:

---
triggers: ["jira", "ticket", "sprint", "backlog", "story", "epic"]
tools_allowed: ["bash", "read_file"]
---

# Jira Workflow

When the user mentions Jira tasks:
1. Use the /create-ticket command to create issues
2. Reference ticket IDs in commit messages (e.g., "PROJ-123: fix login")
3. Update ticket status when tasks complete

hooks/sync-on-complete.sh:

#!/bin/bash
EVENT=$(cat)
SUMMARY=$(echo "$EVENT" | jq -r '.summary')
JIRA_URL=$(vibecli --plugin info vibecody-jira | jq -r '.config.jira_url')
JIRA_TOKEN=$(vibecli --plugin info vibecody-jira | jq -r '.config.jira_token')

curl -s -X POST "$JIRA_URL/rest/api/3/issue" \
  -H "Authorization: Basic $JIRA_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"fields\":{\"summary\":\"$SUMMARY\",\"project\":{\"key\":\"PROJ\"}}}"

exit 0

Example 2: Auto-Formatter Hook

# In ~/.vibecli/config.toml
[[hooks]]
event = "PostToolUse"
tools = ["write_file"]
paths = ["**/*.ts", "**/*.tsx", "**/*.js"]
handler = { command = "npx prettier --write $FILE" }
async = true

Example 3: Custom Skill

---
triggers: ["graphql", "query", "mutation", "schema", "resolver"]
tools_allowed: ["read_file", "write_file", "bash"]
category: api
requires.bins: ["node"]
install.npm: "graphql"
---

# GraphQL Development

When working with GraphQL APIs:

1. **Schema-first design** — Define the schema before resolvers
2. **Use DataLoader** for N+1 prevention
3. **Input validation** — Use custom scalars for emails, dates, URLs
4. **Error handling** — Return typed errors in the `errors` array
5. **Testing** — Use `graphql-tag` for query parsing in tests

Example 4: WASM Extension (Rust)

extension.json:

{
  "name": "word-counter",
  "version": "1.0.0",
  "display_name": "Word Counter",
  "author": "Dev",
  "description": "Shows word count notification on file save",
  "permissions": ["FileRead", "Notify"],
  "wasm_path": "extension.wasm"
}

src/lib.rs:

extern "C" {
    fn log(ptr: *const u8, len: usize);
    fn read_file(path_ptr: *const u8, path_len: usize, out_ptr: *mut u8, out_cap: usize) -> i32;
    fn notify(ptr: *const u8, len: usize);
}

#[no_mangle]
pub extern "C" fn alloc(len: usize) -> *mut u8 {
    let mut buf = Vec::with_capacity(len);
    let ptr = buf.as_mut_ptr();
    std::mem::forget(buf);
    ptr
}

#[no_mangle]
pub extern "C" fn on_file_save(ptr: *const u8, len: usize) {
    let path = unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(ptr, len)) };

    let mut buf = vec![0u8; 1_000_000];
    let bytes_read = unsafe { read_file(path.as_ptr(), path.len(), buf.as_mut_ptr(), buf.len()) };

    if bytes_read > 0 {
        let content = unsafe { std::str::from_utf8_unchecked(&buf[..bytes_read as usize]) };
        let words = content.split_whitespace().count();
        let msg = format!("{}: {} words", path, words);
        unsafe { notify(msg.as_ptr(), msg.len()); }
    }
}

Build:

cargo build --target wasm32-wasi --release
cp target/wasm32-wasi/release/word_counter.wasm ~/.vibeui/extensions/word-counter/extension.wasm

Quick Reference

Creating a Plugin

# 1. Scaffold
vibecli --plugin create my-plugin --kind connector

# 2. Edit plugin.toml, add skills/hooks/commands

# 3. Test locally
vibecli --plugin dev --watch

# 4. Publish
vibecli --plugin publish ./my-plugin

Adding a Skill

# Create a .md file in your project
mkdir -p .vibecli/skills
cat > .vibecli/skills/my-rules.md << 'EOF'
---
triggers: ["react", "component", "hook", "useState"]
tools_allowed: ["read_file", "write_file"]
---

# React Rules

1. Use functional components with hooks
2. Keep components under 200 lines
3. Extract custom hooks for reusable logic
EOF

Adding a Hook

# Add to ~/.vibecli/config.toml
[[hooks]]
event = "PreToolUse"
tools = ["bash"]
handler = { command = "python3 .vibecli/hooks/safety-check.py" }

Connecting an MCP Server

# Add to ~/.vibecli/config.toml
[[mcp_servers]]
name = "my-service"
command = "node"
args = ["my-mcp-server.js"]