Repository files navigation

image

CCC is a launcher for Claude Code that lets you configure prompts, commands, agents, hooks, and MCPs from a single place, in a layered way.

What you get

  • Dynamic Configuration: Generate system/user prompts, commands, agents dynamically.
  • Layered config: Merge global configuration with presets and project overrides.
  • Low‑effort extensibility: write hooks & MCPs in TypeScript with tiny helpers.
image

Not affiliated with Anthropic. Uses the official @anthropic-ai/claude-code CLI.

Warning: Not tested on Windows, open an issue if you run into problems.


Getting Started

1. Setup

# clone this repo somewhere you'd like to keep going to edit your configuration
git clone https://github.com/3rd/ccc.git ~/my-claude-launcher
cd~/my-claude-launcher
# install dependencies and link `ccc`
bun install
bun link
# install tsx globally (required for runtime interception)
bun add -g tsx

Note: Claude Code (@anthropic-ai/claude-code) is included as a dependency.
Bun must also be available on PATH at runtime because CCC launches hook and statusline helpers with bun.
To update it to the latest version do a bun update.

2. Customize your config

Your configuration lives in the ./config directory, which includes some examples by default.

~/my-claude-launcher/ # Your copy of this repository
└── config/
├── global/ # Global configuration
│ ├── prompts/ # System (output style) / user (CLAUDE.md) prompts
│ ├── commands/ # Your commands
│ ├── agents/ # Your sub-agents
│ ├── skills/ # Your skills
│ ├── hooks.ts # Your hooks
│ └── mcps.ts # Your MCPs
├── presets/ # Your language/framework/whatever-specific configs
│ └── typescript/ # Example: TypeScript-specific settings
└── projects/ # Your project-specific overrides
└── myapp/ # Example: Settings for your 'myapp' project

Development Mode: If a ./dev-config directory exists, it will be used instead of ./config. This allows you to keep the example configuration in ./config (committed to git) while using ./dev-config for your actual development configuration.

3. Use it

Your workflow:

  1. Edit your config in ~/my-claude-launcher/config/
  2. Run ccc instead of claude from anywhere
  3. Your config is dynamically built and loaded
ccc # wrap and launch claude
ccc --continue # all the arguments you pass will be passed through to claude# except these special cases used for debugging
ccc --doctor
ccc --print-config
ccc --print-system-prompt
ccc --print-user-prompt
ccc --dump-config
ccc --debug-mcp <mcp-name>
ccc --doru # launcher-only flag; place it before Claude args
ccc --doru --continue

ccc --doru runs CCC through npx doru --ui and auto-opens doru's live UI. Treat --doru as a launcher-only leading flag and place it before any Claude args. The first run may download doru, and doru itself requires npx plus Node.js 22+.

Configuration Layers

ccc loads configurations in layers (later overrides earlier):

  1. Globalconfig/global/ - Base configuration for all projects
  2. Presetsconfig/presets/ - Auto-detected based on project type
  3. Projectsconfig/projects/ - Specific project overrides

Each layer can define:

  • settings.ts - Settings that will go into Claude Code's settings.json
  • prompts/user.{md,ts} - User instructions (CLAUDE.md)
  • prompts/system.{md,ts} - Output style
  • commands/*.{md,ts} - Custom slash commands
  • agents/*.{md,ts} - Custom sub-agents
  • skills/*/SKILL.{md,ts} - Custom skills (and supporting files)
  • hooks.ts - Custom hooks
  • mcps.ts - Custom MCPs

How It Works

ccc injects configurations using a virtual filesystem overlay. Your actual Claude installation remains untouched. Configurations are injected at runtime through Node.js module interception.

The launcher:

  1. Discovers and merges configurations from all layers
  2. Generates a vfs with the merged config
  3. Intercepts Node's modules to serve virtual files
  4. Launches Claude with the injected configuration
Global ┐
Preset ├─► merge ─► "virtual overlay" ─► Claude Code
Project ┘

Using Models from Other Vendors

You can configure CCC to use models from other vendors by setting environment variables in your settings.ts. This allows you to use models like GLM, Kimi K2, or Deepseek through their Anthropic-compatible APIs.

Configuration Examples

Add these environment variables to your config/global/settings.ts:

import{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({env: {// GLM 4.5ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",ANTHROPIC_AUTH_TOKEN: "Z_API_KEY",ANTHROPIC_MODEL: "glm-4.5",ANTHROPIC_FAST_MODEL: "glm-4.5-air",// Kimi K2// ANTHROPIC_BASE_URL: "https://api.moonshot.ai/anthropic",// ANTHROPIC_AUTH_TOKEN: "KIMI_API_KEY",// Deepseek// ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",// ANTHROPIC_AUTH_TOKEN: "DEEPSEEK_API_KEY",// ANTHROPIC_MODEL: "deepseek-chat",// ANTHROPIC_FAST_MODEL: "deepseek-chat"}});

Environment Variables

  • ANTHROPIC_BASE_URL - The base URL for the vendor's API endpoint
  • ANTHROPIC_AUTH_TOKEN - Your API key for the vendor
  • ANTHROPIC_MODEL - The main model to use (e.g., "glm-4.5", "deepseek-chat")
  • ANTHROPIC_FAST_MODEL - The model to use for quick operations (optional)

Usage

Once configured, CCC will automatically use the specified vendor's models instead of Anthropic's models. All CCC features like prompts, commands, agents, hooks, and MCPs will continue to work with the alternative models.

Note: Make sure you have the required API keys and that the vendor's API is compatible with Anthropic's API format.

Extra Configuration

Some settings will still be read from your global ~/.claude.json:

# things like these:
claude config set -g autocheckpointingEnabled true
claude config set -g diffTool delta
claude config set -g supervisorMode true
claude config set -g autoCompactEnabled true
claude config set --global preferredNotifChannel terminal_bell
claude config set -g verbose true

CLI argument settings

Some settings are passed directly as CLI arguments to Claude rather than being written to settings.json. These are grouped under settings.cli. See Claude Code CLI Reference for details.

// config/global/settings.tsimport{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({cli: {tools: ["Bash","Read","Edit","Write"],disallowedTools: ["WebSearch","WebFetch"],allowedTools: ["Read","Glob","Grep"],addDir: ["/path/to/shared/libs"],permissionMode: "plan",verbose: true,debug: "api,hooks",chrome: true,ide: true,enableLspLogging: false,agent: "code-reviewer",},// other settings go to settings.json as normalenv: { ... },});

Available CLI-Only Settings

SettingTypeCLI FlagDescription
toolsstring[] | "default"--tools "Tool1,Tool2"Available tools ("default" for all, [] to disable)
disallowedToolsstring[]--disallowedTools "Tool1,Tool2"Tools removed from model context entirely
allowedToolsstring[]--allowedTools "Tool1,Tool2"Tools that execute without permission prompts
addDirstring[]--add-dir pathAdditional directories Claude can access
permissionModeenum--permission-mode modeStart mode: default, acceptEdits, plan, bypassPermissions
verboseboolean--verboseEnable verbose logging
debugboolean | string--debug [filter]Enable debug mode, optionally with category filter
chromeboolean--chrome / --no-chromeEnable/disable Chrome browser integration
ideboolean--ideAuto-connect to IDE on startup
enableLspLoggingboolean--enable-lsp-loggingEnable verbose LSP logging
agentstring--agent nameDefault agent for the session
agentsRecord<string, AgentDef>--agents JSONCustom subagent definitions
forkSessionboolean--fork-sessionCreate new session ID when resuming
fallbackModelstring--fallback-model nameFallback model when primary is overloaded
settingSourcesstring[]--setting-sources "user,project,local"Config sources to use
strictMcpConfigboolean--strict-mcp-configOnly use specified MCP config

CLI Override

CLI arguments provided directly to ccc override the corresponding settings.cli values:

# override disallowedTools from settings
ccc --disallowedTools "WebSearch,WebFetch"# start in plan mode regardless of settings
ccc --permission-mode plan

System & User Prompts

System Prompt (Output Style)

Controls how Claude responds and behaves.

Static (Markdown) (config/global/prompts/system.md):

You are a helpful coding assistant.
Write clean, maintainable code.
Follow best practices.

Dynamic (TypeScript) (config/global/prompts/system.ts):

import{createPrompt}from"@/config/helpers";exportdefaultcreatePrompt((context)=>`You are working in ${context.workingDirectory}${context.isGitRepo() ? `Current branch: ${context.getGitBranch()}` : ""}Write clean, maintainable code.`,);

Append Mode (adds to previous layers):

import{createAppendPrompt}from"@/config/helpers";exportdefaultcreateAppendPrompt((context)=>`Additional instructions for this preset.`,);

You can also use Markdown files in append mode, just name them: <target>.append.md

User Prompt (CLAUDE.md)

Project-specific instructions and context. See config/global/prompts/user.ts for a full example:

import{createPrompt}from"@/config/helpers";exportdefaultcreatePrompt((context)=>`# CRITICAL RULESDo exactly what the user asks. No alternatives, no "better" solutions...Working in: ${context.workingDirectory}Git branch: ${context.getGitBranch()}`,);

Commands

Custom slash commands available in Claude. See config/global/commands/ for examples:

Static (Markdown) (config/global/commands/review.md):

# Review
Review: "$ARGUMENTS"
You are conducting a code review...

Dynamic (TypeScript):

import{createCommand}from"@/config/helpers";exportdefaultcreateCommand((context)=>`# Custom CommandWorking in ${context.workingDirectory}Current branch: ${context.getGitBranch()}Your command instructions here...`,);

Append to existing command:

import{createAppendCommand}from"@/config/helpers";exportdefaultcreateAppendCommand((context)=>`Additional instructions for TypeScript projects...`,);

Skills

Skills are reusable instruction bundles that Claude can invoke via the Skill tool. Define them as folders under skills/ with either SKILL.md (static) or SKILL.ts (structured) and any supporting files (e.g. references/*.md).

Static (Markdown) (config/global/skills/my-skill/SKILL.md):

---name: my-skilldescription: Quick checks for the current repoallowed-tools:
- Read
- Grep---
Use this skill to run quick repository checks and summarize findings.

Structured (TypeScript) (config/global/skills/my-skill/SKILL.ts):

import{createSkill}from"@/config/helpers";exportdefaultcreateSkill((context)=>({description: `Checks for ${context.project.name}`,content: `Run targeted analysis for ${context.workingDirectory}.Use $ARGUMENTS to accept parameters.`,allowedTools: ["Read","Grep"],userInvocable: true,disableModelInvocation: false,hooks: {PreToolUse: [{matcher: "Bash",hooks: [{type: "command",command: "echo 'Skill hook'"}],},],},}));

Skill layers are resolved global, then preset, then project. A structured skill may set mode: "append" to append its SKILL.md body to an earlier skill with the same name while keeping the base skill metadata; omitted mode means override.

Hooks

Event handlers that run at specific Claude events. See config/global/hooks.ts for examples:

Examples for global hooks:

importpfrom"picocolors";import{createHook}from"@/hooks/hook-generator";import{createConfigHooks}from"@/config/helpers";constbashDenyList=[{match: /^\bgit\bcheckout/,message: "You are not allowed to do checkouts or resets",},{match: /^\bgrep\b(?!.*\|)/,message: "Use 'rg' (ripgrep) instead of 'grep' for better performance",},];constsessionStartHook=createHook({event: "SessionStart",id: "global-session-start",handler: (input)=>{consttimestamp=newDate().toISOString();console.log(p.dim("🞄"));console.log(`🚀 Session started from ${p.yellow(input.source)} at ${p.blue(timestamp)}`,);console.log(`📍 Working directory: ${p.yellow(process.cwd())}`);console.log(`🔧 Node version: ${p.yellow(process.version)}`);console.log(p.dim("🞄"));},});constpreBashValidationHook=createHook({event: "PreToolUse",id: "bash-deny-list",handler: (input)=>{constcommand=input.tool_input.commandasstring;if(input.tool_name!=="Bash"||!command)return;constfirstMatchingRule=bashDenyList.find((rule)=>command.match(rule.match),);if(!firstMatchingRule)return;return{continue: true,decision: "block",reason: firstMatchingRule?.message,};},});exportdefaultcreateConfigHooks({SessionStart: [{hooks: [sessionStartHook]}],PreToolUse: [{hooks: [preBashValidationHook]}],});

Hook Options:

constoneTimeHook=createHook({event: "SessionStart",id: "one-time-setup",handler: (input)=>{// this will only run once per sessionconsole.log("First session start only");},timeout: 5,// optional: timeout in secondsonce: true,// optional: run only first time per session});

TypeScript Validation Example (config/presets/typescript/hooks.ts):

import{$}from"zx";import{createHook}from"@/hooks/hook-generator";import{createConfigHooks}from"@/config/helpers";exportdefaultcreateConfigHooks({Stop: [{hooks: [createHook({event: "Stop",id: "typescript-validation",handler: async()=>{constresult=await$`tsc --noEmit`;if(result.exitCode!==0){return{continue: true,decision: "block",reason: `Failed tsc --noEmit:\n${result.text()}`,};}return{suppressOutput: true};},}),],},],});

Agents

Specialized sub-agents for specific tasks. See config/global/agents/ for examples:

Static (Markdown) (config/global/agents/code-reviewer.md):

---name: code-reviewerdescription: Reviews code for quality and best practicestools: [Read, Grep, Glob, Bash]---# Code Reviewer Agent
You are a specialized code review agent conducting **SYSTEMATIC, EVIDENCE-FIRST CODE REVIEWS**.
## Core Principles**EVIDENCE BEFORE OPINION** - Always provide file:line references...

Dynamic (TypeScript):

import{createAgent}from"@/config/helpers";exportdefaultcreateAgent((context)=>`---name: debuggerdescription: Debug issues in ${context.project.name}tools: [Read, Edit, Bash, Grep, Glob]---# Debugger AgentYou are debugging code in ${context.workingDirectory}Current branch: ${context.getGitBranch()}`,);

MCPs

Model Context Protocol servers for extending Claude's capabilities. See config/global/mcps/ for examples:

External MCPs

import{createConfigMCPs}from"@/config/helpers";exportdefaultcreateConfigMCPs({filesystem: {command: "npx",args: ["@modelcontextprotocol/server-filesystem"],env: {FS_ROOT: "/home/user"},},});

Filtering MCP Tools

You can filter which tools are exposed from an external MCP:

import{createConfigMCPs}from"@/config/helpers";exportdefaultcreateConfigMCPs({nixos: {command: "nix",args: ["run","github:utensils/mcp-nixos","--"],filter: (tool)=>{// Exclude specific toolsreturntool.name!=="nixos_search";},},});

The filter function receives a tool object with name and description properties. Return true to include the tool, false to exclude it.

Custom MCPs

You can easily define custom MCPs in your config using FastMCP.

import{FastMCP}from"fastmcp";import{z}from"zod";import{createConfigMCPs,createMCP}from"@/config/helpers";constcustomTools=createMCP((context)=>{constserver=newFastMCP({name: "custom-tools",version: "1.0.0",});server.addTool({name: "getProjectInfo",description: "Get current project information",parameters: z.object({}),execute: async()=>{returnJSON.stringify({directory: context.workingDirectory,branch: context.getGitBranch(),isGitRepo: context.isGitRepo(),},null,2,);},});returnserver;});exportdefaultcreateConfigMCPs({"custom-tools": customTools,});

Plugins

CCC configures Claude Code plugins via layered plugins.ts files (global/preset/project). Claude plugin settings live under the claude namespace.

Workflow

  1. Install plugins using the /plugin command
  2. Find plugin keys in ~/.claude/plugins/installed_plugins.json
  3. Enable plugins in your plugins.ts layer

Enabling Claude Plugins

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({claude: {enabledPlugins: {// Use keys from ~/.claude/plugins/installed_plugins.json"typescript-lsp@claude-plugins-official": true,"gopls-lsp@claude-plugins-official": true,},},});

Local Plugin Directories

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({claude: {pluginDirs: ["./claude-plugins/my-plugin",],},});

You can also drop local plugins into config/claude-plugins/ and CCC will auto-discover them. CLI override: ccc --plugin-dir ./path/to/plugin

LSP Plugin Support

LSP plugins are supported natively. Just enable your LSP plugins normally:

exportdefaultcreateConfigPlugins({claude: {enabledPlugins: {"typescript-lsp@claude-plugins-official": true,},},});

CCC Plugins

CCC has its own plugin system for bundling reusable configuration components. Unlike Claude's built-in plugins (configured via plugins.ts under claude), CCC plugins are local TypeScript modules that can define commands, agents, MCPs, hooks, and prompts dynamically, and which have access to enriched information about the current session.

CCC Plugins vs Claude Plugins

AspectCCC Plugins (plugins.tsccc)Claude Plugins (plugins.tsclaude)
Locationconfig/plugins/ directory~/.claude/plugins/ (installed) or local roots via config/claude-plugins/
FormatTypeScript with createPlugin()Claude's plugin format
Componentscommands, agents, MCPs, hooks, promptsDefined by Claude
DistributionLocal to your configVia /plugin command

Plugin Structure

A CCC plugin requires two files:

config/plugins/my-plugin/
├── plugin.json # Manifest with name, version, description
└── index.ts # Plugin definition using createPlugin()

plugin.json (manifest):

{
"name": "my-plugin",
"version": "1.0.0",
"description": "A custom CCC plugin"
}

index.ts (definition):

import{createPlugin}from"@/config/helpers";exportdefaultcreatePlugin({// Commands - custom slash commandscommands: (context)=>({"my-command": {content: `# My CommandYour command instructions here... `.trim(),mode: "override",},}),// Agents - specialized sub-agentsagents: (context)=>({"my-agent": {content: `---name: my-agentdescription: A specialized agenttools: [Read, Grep, Glob]---Agent instructions... `.trim(),mode: "override",},}),// MCPs - inline MCP serversmcps: (context)=>({"my-mcp": {type: "inline",config: ()=>createMyMCP(context),},}),// Hooks - event handlershooks: (context)=>({Stop: [{hooks: [createHook({event: "Stop",id: "plugin-stop-hook",handler: ()=>{// Hook logic},}),],},],}),// Prompts - system/user prompt additionsprompts: (context)=>({user: {content: "Additional user prompt content",mode: "append",},}),});

Enabling CCC Plugins

Enable CCC plugins via plugins.ts:

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"my-plugin": true,// Enable plugin"another-plugin": false,// Explicitly disable},});

You can also enable plugins in presets:

// config/presets/typescript/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"typescript-helpers": true,},});

Plugin Context

Plugins receive a PluginContext with access to:

{// Standard context properties
workingDirectory: string;
launcherDirectory: string;isGitRepo(): boolean;getGitBranch(): string;// ... all Context methods// Plugin-specific
manifest: PluginManifest;// Plugin's plugin.json
root: string;// Plugin directory path
settings: Record<string,unknown>;// Plugin settings// State management
state: {get<T>(key: string): T|undefined;set(key: string,value: unknown): void;clear(): void;getAll(): Record<string,unknown>;};}

Plugin State

Plugins can persist state using the built-in state API. State is automatically saved to disk and restored between sessions.

State API:

context.state.get<T>(key: string): T|undefined// Get a valuecontext.state.set(key: string,value: unknown): void// Set a valuecontext.state.clear(): void// Clear all statecontext.state.getAll(): Record<string,unknown>// Get all state

State Locations:

By default, plugin state is stored in /tmp/ccc-plugin-{name}-{sessionId}.json. The location is isolated by session ID (CCC_INSTANCE_ID) to prevent conflicts between concurrent CCC instances.

LocationPathUse Case
temp (default)/tmp/ccc-plugin-{name}-{sessionId}.jsonSession-scoped data
project{cwd}/.ccc/state/plugins/{name}.jsonProject-specific persistent data
user~/.ccc/state/plugins/{name}.jsonGlobal user preferences

Example - Stateful MCP:

exportdefaultcreatePlugin({mcps: (context)=>({"stateful-mcp": {type: "inline",config: ()=>{constserver=newFastMCP({name: "stateful",version: "1.0.0"});server.addTool({name: "save_data",parameters: z.object({key: z.string(),value: z.string()}),execute: async(args)=>{context.state.set(args.key,args.value);return"Saved!";},});server.addTool({name: "load_data",parameters: z.object({key: z.string()}),execute: async(args)=>{returncontext.state.get(args.key)??"Not found";},});returnserver;},},}),});

Plugin Settings

Plugins define settings using Zod schemas in index.ts. The type is automatically inferred:

// my-plugin/index.tsimport{z}from"zod";import{createPlugin}from"@/config/helpers";constsettingsSchema=z.object({maxItems: z.number().default(100),mode: z.enum(["fast","balanced","thorough"]).default("balanced"),});exportdefaultcreatePlugin({
settingsSchema,mcps: (context)=>{// context.plugin.settings is typed as { maxItems: number, mode: "fast" | "balanced" | "thorough" }const{ maxItems, mode }=context.plugin.settings;// ...},});

Pass settings when enabling the plugin:

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"my-plugin": {enabled: true,settings: {maxItems: 50,mode: "fast",},},},});

Settings are validated with Zod at plugin load time. Invalid settings throw errors.

Plugin State

Plugins get a context.state API for key-value storage:

context.state.get<T>(key)// get valuecontext.state.set(key,value)// set valuecontext.state.clear()// clear all statecontext.state.getAll()// get all state

By default, state is in-memory only (not persisted). To persist state, set stateType:

exportdefaultcreatePlugin({stateType: "temp",// /tmp/ccc-plugin-{name}-{sessionId}.json (per-instance)// or:stateType: "project",// {projectRoot}/.ccc/state/plugins/{name}.json// or:stateType: "user",// ~/.ccc/state/plugins/{name}.jsonmcps: (context)=>{/* ... */},});
stateTypePathUse case
"none" (default)(in-memory only)No persistence needed
"temp"/tmp/ccc-plugin-{name}-{sessionId}.jsonPer-instance state, cleared on reboot
"project"{projectRoot}/.ccc/state/plugins/{name}.jsonProject-specific persistent state
"user"~/.ccc/state/plugins/{name}.jsonUser-wide persistent state

onLoad Hook

Plugins can define an onLoad callback for initialization:

exportdefaultcreatePlugin({onLoad: async(context)=>{console.log(`Plugin ${context.plugin.name} loaded`);},commands: ()=>({/* ... */}),});

Inter-Plugin Communication

Plugins can access other loaded plugins using getPlugin():

exportdefaultcreatePlugin({mcps: (context)=>({"my-mcp": {type: "inline",config: ()=>{constserver=newFastMCP({name: "my-mcp",version: "1.0.0"});server.addTool({name: "get_other_plugin_data",parameters: z.object({}),execute: async()=>{// Access another plugin's contextconstotherPlugin=context.getPlugin("other-plugin");if(!otherPlugin)return"Other plugin not loaded";// Read its stateconstdata=otherPlugin.state.get("shared-data");returnJSON.stringify(data);},});returnserver;},},}),});

Plugin Dependencies

Plugins can declare dependencies on other plugins in plugin.json:

{
"name": "my-plugin",
"version": "1.0.0",
"description": "Depends on base-plugin",
"dependencies": ["base-plugin", "utility-plugin"]
}

Dependencies are loaded first, ensuring they're available when your plugin loads.

Full plugin.json Schema

{
"name": "my-plugin",
"version": "1.0.0",
"description": "A complete example plugin",
"author": "Your Name",
"license": "MIT",
"homepage": "https://example.com/my-plugin",
"repository": "https://github.com/user/my-plugin",
"dependencies": ["other-plugin"]
}

Note: Settings are defined via Zod schema in index.ts, not in plugin.json.

Example: Complete Plugin

Here's a complete example of a plugin with commands, an MCP, and hooks:

// config/plugins/task-tracker/index.tsimport{FastMCP}from"fastmcp";import{z}from"zod";import{createPlugin}from"@/config/helpers";import{createHook}from"@/hooks/hook-generator";exportdefaultcreatePlugin({commands: ()=>({"track": {content: `# Task TrackerTrack a new task: "$ARGUMENTS"Use the task_add MCP tool to add this task. `.trim(),mode: "override",},}),mcps: (context)=>({"task-tracker": {type: "inline",config: ()=>{constserver=newFastMCP({name: "task-tracker",version: "1.0.0",});server.addTool({name: "task_add",description: "Add a new task",parameters: z.object({title: z.string(),priority: z.enum(["low","medium","high"]).default("medium"),}),execute: async(args)=>{consttasks=context.state.get<string[]>("tasks")??[];tasks.push(`[${args.priority}] ${args.title}`);context.state.set("tasks",tasks);return`Added: ${args.title}`;},});server.addTool({name: "task_list",description: "List all tasks",parameters: z.object({}),execute: async()=>{consttasks=context.state.get<string[]>("tasks")??[];returntasks.length>0 ? tasks.join("\n") : "No tasks";},});returnserver;},},}),hooks: ()=>({SessionStart: [{hooks: [createHook({event: "SessionStart",id: "task-tracker-init",handler: ()=>{console.log("📋 Task Tracker plugin loaded");},}),],},],}),});

Viewing Plugin Info

Use ccc --print-config to see loaded CCC plugins:

CCC Plugins:
my-plugin (v1.0.0) [enabled]
Commands: my-plugin:my-command
MCPs: my-plugin:my-mcp
Hooks: Stop(1)

Note: Plugin components are namespaced with the plugin name (e.g., my-plugin:my-command).

Runtime Patches

All CLI patches are applied at runtime - the original node_modules files are never modified. The launcher reads the CLI, applies patches, writes to a temp file, and imports that instead.

Built-in Patches

Applied automatically on every launch:

  • Disable pr-comments and security-review features

User-defined Patches

Add custom string replacements via settings:

exportdefaultcreateConfigSettings({patches: [{find: "ultrathink",replace: "uuu"},// shorter alias],});

Patches are applied after built-in patches. No reinstall needed when changing configuration.

Statusline

Customize the Claude statusline with a simple configuration-based approach.

Priority Order

  1. config/global/statusline.ts - If this file exists, it will be executed with bun
  2. settings.statusLine - Otherwise, use the statusLine configuration from settings
  3. None - If neither is configured, no statusline is displayed

Creating a Statusline

Create config/global/statusline.ts:

import{createStatusline}from"@/config/helpers";importtype{StatusLineInput}from"@/types/statusline";exportdefaultcreateStatusline(async(data: StatusLineInput)=>{constmodelIcon=data.model?.id?.includes("opus") ? "🦆" : "🐇";constcomponents=[];// Model and iconcomponents.push(`${modelIcon}${data.model.display_name}`);// Working directoryif(data.workspace){constdir=data.workspace.project_dir||data.workspace.current_dir;constshortDir=dir.split("/").slice(-2).join("/");components.push(`📁 ${shortDir}`);}// Hook event (if present)if(data.hook_event_name){components.push(`⚡ ${data.hook_event_name}`);}console.log(components.join(" │ "));});

Using External Tools

You can also integrate external statusline tools:

import{createStatusline}from"@/config/helpers";import{$}from"bun";exportdefaultcreateStatusline(async(data)=>{// Use external ccstatusline toolconstoutput=await$`echo ${JSON.stringify(data)} | bunx ccstatusline`.text();constmodelIcon=data.model?.id?.includes("opus") ? "🦆" : "🐇";console.log(`${modelIcon}${output.trim()}`);});

StatusLineInput Type

The statusline function receives a StatusLineInput object with:

  • model.id - Model identifier (e.g., "claude-3-opus-20240229")
  • model.display_name - Human-readable model name
  • workspace.current_dir - Current working directory
  • workspace.project_dir - Project root directory
  • hook_event_name - Current hook event being executed
  • session_id - Current session identifier
  • transcript_path - Path to the transcript file
  • cwd - Current working directory
  • output_style - Output style configuration

Settings Configuration

Alternatively, configure a custom statusline command in settings.ts:

exportdefaultcreateConfigSettings({statusLine: {type: "command",command: "/path/to/your/statusline-script",},});

Note: Unlike other configuration types, statuslines do NOT support layering or merging. Only the global configuration or settings are used.

Doctor (Config Inspector)

Use ccc --doctor to print a diagnostic report of your merged configuration without launching Claude:

ccc --doctor
ccc --doctor --json

The report shows:

  • Presets detected and project configuration in use
  • Layering traces (override/append) for system/user prompts
  • Per-command and per-agent layering traces across global/presets/project
  • MCP servers and their transport type

Dump Configuration

Use ccc --dump-config to create a complete dump of the computed configuration that Claude sees:

ccc --dump-config

This creates a .config-dump/{timestamp}/ directory containing:

  • system.md - The actual computed system prompt
  • user.md - The actual computed user prompt
  • commands/ - All command files as Claude sees them
  • agents/ - All agent files as Claude sees them
  • settings.json - The merged settings
  • mcps.json - The computed MCP configurations
  • metadata.json - Context and dump information

This is useful for debugging configuration issues and understanding exactly what Claude sees.

Debug MCPs

Use ccc --debug-mcp <mcp-name> to launch the MCP Inspector for debugging MCP servers:

ccc --debug-mcp filesystem
ccc --debug-mcp custom-tools

This launches the MCP Inspector with your MCP server, allowing you to:

  • View all available tools, resources, and prompts
  • Test tool invocations interactively
  • Inspect request/response payloads
  • Debug filtered MCPs (shows tools after filtering)

Note:

  • Only works with stdio transport MCPs (not HTTP/SSE)
  • Filtered MCPs will show the filtered tools, not the original ones
  • Inline MCPs (created with FastMCP) are supported

Project Configuration

Create a project-specific configuration:

// config/projects/myapp/project.tsexportdefault{name: "myapp",root: "/path/to/myapp",disableParentClaudeMds: false,// optional, will disable Claude's behavior of loading upper CLAUDE.md files};
// config/projects/myapp/settings.tsimport{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({env: {NODE_ENV: "development",API_URL: "http://localhost:3000",},});

Context Object

All dynamic configurations receive a context object with a few utilities:

{
workingDirectory: string;// Current working directory
launcherDirectory: string;// Path to launcher installation
instanceId: string;// Unique instance identifier
project: Project;// Project instance with config
mcpServers?: Record<string,ClaudeMCPConfig>;// Processed MCP configs for this runisGitRepo(): boolean;// Check if in git repositorygetGitBranch(): string;// Current git branchgetGitStatus(): string;// Git status (porcelain)getGitRecentCommits(n): string;// Recent commit historygetDirectoryTree(): string;// Directory structuregetPlatform(): string;// OS platformgetOsVersion(): string;// OS version infogetCurrentDateTime(): string;// ISO timestamphasMCP(name: string): boolean;// True if MCP with name is configured}

Other things

  • ?

License

MIT License. See LICENSE for details.

About

Custom launcher for Claude Code, supporting dynamic prompts, layered configuration and easy custom hooks and MCPs.

Topics

Resources

Stars

19 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

image

CCC is a launcher for Claude Code that lets you configure prompts, commands, agents, hooks, and MCPs from a single place, in a layered way.

What you get

  • Dynamic Configuration: Generate system/user prompts, commands, agents dynamically.
  • Layered config: Merge global configuration with presets and project overrides.
  • Low‑effort extensibility: write hooks & MCPs in TypeScript with tiny helpers.
image

Not affiliated with Anthropic. Uses the official @anthropic-ai/claude-code CLI.

Warning: Not tested on Windows, open an issue if you run into problems.


Getting Started

1. Setup

# clone this repo somewhere you'd like to keep going to edit your configuration
git clone https://github.com/3rd/ccc.git ~/my-claude-launcher
cd~/my-claude-launcher
# install dependencies and link `ccc`
bun install
bun link
# install tsx globally (required for runtime interception)
bun add -g tsx

Note: Claude Code (@anthropic-ai/claude-code) is included as a dependency.
Bun must also be available on PATH at runtime because CCC launches hook and statusline helpers with bun.
To update it to the latest version do a bun update.

2. Customize your config

Your configuration lives in the ./config directory, which includes some examples by default.

~/my-claude-launcher/ # Your copy of this repository
└── config/
├── global/ # Global configuration
│ ├── prompts/ # System (output style) / user (CLAUDE.md) prompts
│ ├── commands/ # Your commands
│ ├── agents/ # Your sub-agents
│ ├── skills/ # Your skills
│ ├── hooks.ts # Your hooks
│ └── mcps.ts # Your MCPs
├── presets/ # Your language/framework/whatever-specific configs
│ └── typescript/ # Example: TypeScript-specific settings
└── projects/ # Your project-specific overrides
└── myapp/ # Example: Settings for your 'myapp' project

Development Mode: If a ./dev-config directory exists, it will be used instead of ./config. This allows you to keep the example configuration in ./config (committed to git) while using ./dev-config for your actual development configuration.

3. Use it

Your workflow:

  1. Edit your config in ~/my-claude-launcher/config/
  2. Run ccc instead of claude from anywhere
  3. Your config is dynamically built and loaded
ccc # wrap and launch claude
ccc --continue # all the arguments you pass will be passed through to claude# except these special cases used for debugging
ccc --doctor
ccc --print-config
ccc --print-system-prompt
ccc --print-user-prompt
ccc --dump-config
ccc --debug-mcp <mcp-name>
ccc --doru # launcher-only flag; place it before Claude args
ccc --doru --continue

ccc --doru runs CCC through npx doru --ui and auto-opens doru's live UI. Treat --doru as a launcher-only leading flag and place it before any Claude args. The first run may download doru, and doru itself requires npx plus Node.js 22+.

Configuration Layers

ccc loads configurations in layers (later overrides earlier):

  1. Globalconfig/global/ - Base configuration for all projects
  2. Presetsconfig/presets/ - Auto-detected based on project type
  3. Projectsconfig/projects/ - Specific project overrides

Each layer can define:

  • settings.ts - Settings that will go into Claude Code's settings.json
  • prompts/user.{md,ts} - User instructions (CLAUDE.md)
  • prompts/system.{md,ts} - Output style
  • commands/*.{md,ts} - Custom slash commands
  • agents/*.{md,ts} - Custom sub-agents
  • skills/*/SKILL.{md,ts} - Custom skills (and supporting files)
  • hooks.ts - Custom hooks
  • mcps.ts - Custom MCPs

How It Works

ccc injects configurations using a virtual filesystem overlay. Your actual Claude installation remains untouched. Configurations are injected at runtime through Node.js module interception.

The launcher:

  1. Discovers and merges configurations from all layers
  2. Generates a vfs with the merged config
  3. Intercepts Node's modules to serve virtual files
  4. Launches Claude with the injected configuration
Global ┐
Preset ├─► merge ─► "virtual overlay" ─► Claude Code
Project ┘

Using Models from Other Vendors

You can configure CCC to use models from other vendors by setting environment variables in your settings.ts. This allows you to use models like GLM, Kimi K2, or Deepseek through their Anthropic-compatible APIs.

Configuration Examples

Add these environment variables to your config/global/settings.ts:

import{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({env: {// GLM 4.5ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",ANTHROPIC_AUTH_TOKEN: "Z_API_KEY",ANTHROPIC_MODEL: "glm-4.5",ANTHROPIC_FAST_MODEL: "glm-4.5-air",// Kimi K2// ANTHROPIC_BASE_URL: "https://api.moonshot.ai/anthropic",// ANTHROPIC_AUTH_TOKEN: "KIMI_API_KEY",// Deepseek// ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",// ANTHROPIC_AUTH_TOKEN: "DEEPSEEK_API_KEY",// ANTHROPIC_MODEL: "deepseek-chat",// ANTHROPIC_FAST_MODEL: "deepseek-chat"}});

Environment Variables

  • ANTHROPIC_BASE_URL - The base URL for the vendor's API endpoint
  • ANTHROPIC_AUTH_TOKEN - Your API key for the vendor
  • ANTHROPIC_MODEL - The main model to use (e.g., "glm-4.5", "deepseek-chat")
  • ANTHROPIC_FAST_MODEL - The model to use for quick operations (optional)

Usage

Once configured, CCC will automatically use the specified vendor's models instead of Anthropic's models. All CCC features like prompts, commands, agents, hooks, and MCPs will continue to work with the alternative models.

Note: Make sure you have the required API keys and that the vendor's API is compatible with Anthropic's API format.

Extra Configuration

Some settings will still be read from your global ~/.claude.json:

# things like these:
claude config set -g autocheckpointingEnabled true
claude config set -g diffTool delta
claude config set -g supervisorMode true
claude config set -g autoCompactEnabled true
claude config set --global preferredNotifChannel terminal_bell
claude config set -g verbose true

CLI argument settings

Some settings are passed directly as CLI arguments to Claude rather than being written to settings.json. These are grouped under settings.cli. See Claude Code CLI Reference for details.

// config/global/settings.tsimport{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({cli: {tools: ["Bash","Read","Edit","Write"],disallowedTools: ["WebSearch","WebFetch"],allowedTools: ["Read","Glob","Grep"],addDir: ["/path/to/shared/libs"],permissionMode: "plan",verbose: true,debug: "api,hooks",chrome: true,ide: true,enableLspLogging: false,agent: "code-reviewer",},// other settings go to settings.json as normalenv: { ... },});

Available CLI-Only Settings

SettingTypeCLI FlagDescription
toolsstring[] | "default"--tools "Tool1,Tool2"Available tools ("default" for all, [] to disable)
disallowedToolsstring[]--disallowedTools "Tool1,Tool2"Tools removed from model context entirely
allowedToolsstring[]--allowedTools "Tool1,Tool2"Tools that execute without permission prompts
addDirstring[]--add-dir pathAdditional directories Claude can access
permissionModeenum--permission-mode modeStart mode: default, acceptEdits, plan, bypassPermissions
verboseboolean--verboseEnable verbose logging
debugboolean | string--debug [filter]Enable debug mode, optionally with category filter
chromeboolean--chrome / --no-chromeEnable/disable Chrome browser integration
ideboolean--ideAuto-connect to IDE on startup
enableLspLoggingboolean--enable-lsp-loggingEnable verbose LSP logging
agentstring--agent nameDefault agent for the session
agentsRecord<string, AgentDef>--agents JSONCustom subagent definitions
forkSessionboolean--fork-sessionCreate new session ID when resuming
fallbackModelstring--fallback-model nameFallback model when primary is overloaded
settingSourcesstring[]--setting-sources "user,project,local"Config sources to use
strictMcpConfigboolean--strict-mcp-configOnly use specified MCP config

CLI Override

CLI arguments provided directly to ccc override the corresponding settings.cli values:

# override disallowedTools from settings
ccc --disallowedTools "WebSearch,WebFetch"# start in plan mode regardless of settings
ccc --permission-mode plan

System & User Prompts

System Prompt (Output Style)

Controls how Claude responds and behaves.

Static (Markdown) (config/global/prompts/system.md):

You are a helpful coding assistant.
Write clean, maintainable code.
Follow best practices.

Dynamic (TypeScript) (config/global/prompts/system.ts):

import{createPrompt}from"@/config/helpers";exportdefaultcreatePrompt((context)=>`You are working in ${context.workingDirectory}${context.isGitRepo() ? `Current branch: ${context.getGitBranch()}` : ""}Write clean, maintainable code.`,);

Append Mode (adds to previous layers):

import{createAppendPrompt}from"@/config/helpers";exportdefaultcreateAppendPrompt((context)=>`Additional instructions for this preset.`,);

You can also use Markdown files in append mode, just name them: <target>.append.md

User Prompt (CLAUDE.md)

Project-specific instructions and context. See config/global/prompts/user.ts for a full example:

import{createPrompt}from"@/config/helpers";exportdefaultcreatePrompt((context)=>`# CRITICAL RULESDo exactly what the user asks. No alternatives, no "better" solutions...Working in: ${context.workingDirectory}Git branch: ${context.getGitBranch()}`,);

Commands

Custom slash commands available in Claude. See config/global/commands/ for examples:

Static (Markdown) (config/global/commands/review.md):

# Review
Review: "$ARGUMENTS"
You are conducting a code review...

Dynamic (TypeScript):

import{createCommand}from"@/config/helpers";exportdefaultcreateCommand((context)=>`# Custom CommandWorking in ${context.workingDirectory}Current branch: ${context.getGitBranch()}Your command instructions here...`,);

Append to existing command:

import{createAppendCommand}from"@/config/helpers";exportdefaultcreateAppendCommand((context)=>`Additional instructions for TypeScript projects...`,);

Skills

Skills are reusable instruction bundles that Claude can invoke via the Skill tool. Define them as folders under skills/ with either SKILL.md (static) or SKILL.ts (structured) and any supporting files (e.g. references/*.md).

Static (Markdown) (config/global/skills/my-skill/SKILL.md):

---name: my-skilldescription: Quick checks for the current repoallowed-tools:
- Read
- Grep---
Use this skill to run quick repository checks and summarize findings.

Structured (TypeScript) (config/global/skills/my-skill/SKILL.ts):

import{createSkill}from"@/config/helpers";exportdefaultcreateSkill((context)=>({description: `Checks for ${context.project.name}`,content: `Run targeted analysis for ${context.workingDirectory}.Use $ARGUMENTS to accept parameters.`,allowedTools: ["Read","Grep"],userInvocable: true,disableModelInvocation: false,hooks: {PreToolUse: [{matcher: "Bash",hooks: [{type: "command",command: "echo 'Skill hook'"}],},],},}));

Skill layers are resolved global, then preset, then project. A structured skill may set mode: "append" to append its SKILL.md body to an earlier skill with the same name while keeping the base skill metadata; omitted mode means override.

Hooks

Event handlers that run at specific Claude events. See config/global/hooks.ts for examples:

Examples for global hooks:

importpfrom"picocolors";import{createHook}from"@/hooks/hook-generator";import{createConfigHooks}from"@/config/helpers";constbashDenyList=[{match: /^\bgit\bcheckout/,message: "You are not allowed to do checkouts or resets",},{match: /^\bgrep\b(?!.*\|)/,message: "Use 'rg' (ripgrep) instead of 'grep' for better performance",},];constsessionStartHook=createHook({event: "SessionStart",id: "global-session-start",handler: (input)=>{consttimestamp=newDate().toISOString();console.log(p.dim("🞄"));console.log(`🚀 Session started from ${p.yellow(input.source)} at ${p.blue(timestamp)}`,);console.log(`📍 Working directory: ${p.yellow(process.cwd())}`);console.log(`🔧 Node version: ${p.yellow(process.version)}`);console.log(p.dim("🞄"));},});constpreBashValidationHook=createHook({event: "PreToolUse",id: "bash-deny-list",handler: (input)=>{constcommand=input.tool_input.commandasstring;if(input.tool_name!=="Bash"||!command)return;constfirstMatchingRule=bashDenyList.find((rule)=>command.match(rule.match),);if(!firstMatchingRule)return;return{continue: true,decision: "block",reason: firstMatchingRule?.message,};},});exportdefaultcreateConfigHooks({SessionStart: [{hooks: [sessionStartHook]}],PreToolUse: [{hooks: [preBashValidationHook]}],});

Hook Options:

constoneTimeHook=createHook({event: "SessionStart",id: "one-time-setup",handler: (input)=>{// this will only run once per sessionconsole.log("First session start only");},timeout: 5,// optional: timeout in secondsonce: true,// optional: run only first time per session});

TypeScript Validation Example (config/presets/typescript/hooks.ts):

import{$}from"zx";import{createHook}from"@/hooks/hook-generator";import{createConfigHooks}from"@/config/helpers";exportdefaultcreateConfigHooks({Stop: [{hooks: [createHook({event: "Stop",id: "typescript-validation",handler: async()=>{constresult=await$`tsc --noEmit`;if(result.exitCode!==0){return{continue: true,decision: "block",reason: `Failed tsc --noEmit:\n${result.text()}`,};}return{suppressOutput: true};},}),],},],});

Agents

Specialized sub-agents for specific tasks. See config/global/agents/ for examples:

Static (Markdown) (config/global/agents/code-reviewer.md):

---name: code-reviewerdescription: Reviews code for quality and best practicestools: [Read, Grep, Glob, Bash]---# Code Reviewer Agent
You are a specialized code review agent conducting **SYSTEMATIC, EVIDENCE-FIRST CODE REVIEWS**.
## Core Principles**EVIDENCE BEFORE OPINION** - Always provide file:line references...

Dynamic (TypeScript):

import{createAgent}from"@/config/helpers";exportdefaultcreateAgent((context)=>`---name: debuggerdescription: Debug issues in ${context.project.name}tools: [Read, Edit, Bash, Grep, Glob]---# Debugger AgentYou are debugging code in ${context.workingDirectory}Current branch: ${context.getGitBranch()}`,);

MCPs

Model Context Protocol servers for extending Claude's capabilities. See config/global/mcps/ for examples:

External MCPs

import{createConfigMCPs}from"@/config/helpers";exportdefaultcreateConfigMCPs({filesystem: {command: "npx",args: ["@modelcontextprotocol/server-filesystem"],env: {FS_ROOT: "/home/user"},},});

Filtering MCP Tools

You can filter which tools are exposed from an external MCP:

import{createConfigMCPs}from"@/config/helpers";exportdefaultcreateConfigMCPs({nixos: {command: "nix",args: ["run","github:utensils/mcp-nixos","--"],filter: (tool)=>{// Exclude specific toolsreturntool.name!=="nixos_search";},},});

The filter function receives a tool object with name and description properties. Return true to include the tool, false to exclude it.

Custom MCPs

You can easily define custom MCPs in your config using FastMCP.

import{FastMCP}from"fastmcp";import{z}from"zod";import{createConfigMCPs,createMCP}from"@/config/helpers";constcustomTools=createMCP((context)=>{constserver=newFastMCP({name: "custom-tools",version: "1.0.0",});server.addTool({name: "getProjectInfo",description: "Get current project information",parameters: z.object({}),execute: async()=>{returnJSON.stringify({directory: context.workingDirectory,branch: context.getGitBranch(),isGitRepo: context.isGitRepo(),},null,2,);},});returnserver;});exportdefaultcreateConfigMCPs({"custom-tools": customTools,});

Plugins

CCC configures Claude Code plugins via layered plugins.ts files (global/preset/project). Claude plugin settings live under the claude namespace.

Workflow

  1. Install plugins using the /plugin command
  2. Find plugin keys in ~/.claude/plugins/installed_plugins.json
  3. Enable plugins in your plugins.ts layer

Enabling Claude Plugins

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({claude: {enabledPlugins: {// Use keys from ~/.claude/plugins/installed_plugins.json"typescript-lsp@claude-plugins-official": true,"gopls-lsp@claude-plugins-official": true,},},});

Local Plugin Directories

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({claude: {pluginDirs: ["./claude-plugins/my-plugin",],},});

You can also drop local plugins into config/claude-plugins/ and CCC will auto-discover them. CLI override: ccc --plugin-dir ./path/to/plugin

LSP Plugin Support

LSP plugins are supported natively. Just enable your LSP plugins normally:

exportdefaultcreateConfigPlugins({claude: {enabledPlugins: {"typescript-lsp@claude-plugins-official": true,},},});

CCC Plugins

CCC has its own plugin system for bundling reusable configuration components. Unlike Claude's built-in plugins (configured via plugins.ts under claude), CCC plugins are local TypeScript modules that can define commands, agents, MCPs, hooks, and prompts dynamically, and which have access to enriched information about the current session.

CCC Plugins vs Claude Plugins

AspectCCC Plugins (plugins.tsccc)Claude Plugins (plugins.tsclaude)
Locationconfig/plugins/ directory~/.claude/plugins/ (installed) or local roots via config/claude-plugins/
FormatTypeScript with createPlugin()Claude's plugin format
Componentscommands, agents, MCPs, hooks, promptsDefined by Claude
DistributionLocal to your configVia /plugin command

Plugin Structure

A CCC plugin requires two files:

config/plugins/my-plugin/
├── plugin.json # Manifest with name, version, description
└── index.ts # Plugin definition using createPlugin()

plugin.json (manifest):

{
"name": "my-plugin",
"version": "1.0.0",
"description": "A custom CCC plugin"
}

index.ts (definition):

import{createPlugin}from"@/config/helpers";exportdefaultcreatePlugin({// Commands - custom slash commandscommands: (context)=>({"my-command": {content: `# My CommandYour command instructions here... `.trim(),mode: "override",},}),// Agents - specialized sub-agentsagents: (context)=>({"my-agent": {content: `---name: my-agentdescription: A specialized agenttools: [Read, Grep, Glob]---Agent instructions... `.trim(),mode: "override",},}),// MCPs - inline MCP serversmcps: (context)=>({"my-mcp": {type: "inline",config: ()=>createMyMCP(context),},}),// Hooks - event handlershooks: (context)=>({Stop: [{hooks: [createHook({event: "Stop",id: "plugin-stop-hook",handler: ()=>{// Hook logic},}),],},],}),// Prompts - system/user prompt additionsprompts: (context)=>({user: {content: "Additional user prompt content",mode: "append",},}),});

Enabling CCC Plugins

Enable CCC plugins via plugins.ts:

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"my-plugin": true,// Enable plugin"another-plugin": false,// Explicitly disable},});

You can also enable plugins in presets:

// config/presets/typescript/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"typescript-helpers": true,},});

Plugin Context

Plugins receive a PluginContext with access to:

{// Standard context properties
workingDirectory: string;
launcherDirectory: string;isGitRepo(): boolean;getGitBranch(): string;// ... all Context methods// Plugin-specific
manifest: PluginManifest;// Plugin's plugin.json
root: string;// Plugin directory path
settings: Record<string,unknown>;// Plugin settings// State management
state: {get<T>(key: string): T|undefined;set(key: string,value: unknown): void;clear(): void;getAll(): Record<string,unknown>;};}

Plugin State

Plugins can persist state using the built-in state API. State is automatically saved to disk and restored between sessions.

State API:

context.state.get<T>(key: string): T|undefined// Get a valuecontext.state.set(key: string,value: unknown): void// Set a valuecontext.state.clear(): void// Clear all statecontext.state.getAll(): Record<string,unknown>// Get all state

State Locations:

By default, plugin state is stored in /tmp/ccc-plugin-{name}-{sessionId}.json. The location is isolated by session ID (CCC_INSTANCE_ID) to prevent conflicts between concurrent CCC instances.

LocationPathUse Case
temp (default)/tmp/ccc-plugin-{name}-{sessionId}.jsonSession-scoped data
project{cwd}/.ccc/state/plugins/{name}.jsonProject-specific persistent data
user~/.ccc/state/plugins/{name}.jsonGlobal user preferences

Example - Stateful MCP:

exportdefaultcreatePlugin({mcps: (context)=>({"stateful-mcp": {type: "inline",config: ()=>{constserver=newFastMCP({name: "stateful",version: "1.0.0"});server.addTool({name: "save_data",parameters: z.object({key: z.string(),value: z.string()}),execute: async(args)=>{context.state.set(args.key,args.value);return"Saved!";},});server.addTool({name: "load_data",parameters: z.object({key: z.string()}),execute: async(args)=>{returncontext.state.get(args.key)??"Not found";},});returnserver;},},}),});

Plugin Settings

Plugins define settings using Zod schemas in index.ts. The type is automatically inferred:

// my-plugin/index.tsimport{z}from"zod";import{createPlugin}from"@/config/helpers";constsettingsSchema=z.object({maxItems: z.number().default(100),mode: z.enum(["fast","balanced","thorough"]).default("balanced"),});exportdefaultcreatePlugin({
settingsSchema,mcps: (context)=>{// context.plugin.settings is typed as { maxItems: number, mode: "fast" | "balanced" | "thorough" }const{ maxItems, mode }=context.plugin.settings;// ...},});

Pass settings when enabling the plugin:

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"my-plugin": {enabled: true,settings: {maxItems: 50,mode: "fast",},},},});

Settings are validated with Zod at plugin load time. Invalid settings throw errors.

Plugin State

Plugins get a context.state API for key-value storage:

context.state.get<T>(key)// get valuecontext.state.set(key,value)// set valuecontext.state.clear()// clear all statecontext.state.getAll()// get all state

By default, state is in-memory only (not persisted). To persist state, set stateType:

exportdefaultcreatePlugin({stateType: "temp",// /tmp/ccc-plugin-{name}-{sessionId}.json (per-instance)// or:stateType: "project",// {projectRoot}/.ccc/state/plugins/{name}.json// or:stateType: "user",// ~/.ccc/state/plugins/{name}.jsonmcps: (context)=>{/* ... */},});
stateTypePathUse case
"none" (default)(in-memory only)No persistence needed
"temp"/tmp/ccc-plugin-{name}-{sessionId}.jsonPer-instance state, cleared on reboot
"project"{projectRoot}/.ccc/state/plugins/{name}.jsonProject-specific persistent state
"user"~/.ccc/state/plugins/{name}.jsonUser-wide persistent state

onLoad Hook

Plugins can define an onLoad callback for initialization:

exportdefaultcreatePlugin({onLoad: async(context)=>{console.log(`Plugin ${context.plugin.name} loaded`);},commands: ()=>({/* ... */}),});

Inter-Plugin Communication

Plugins can access other loaded plugins using getPlugin():

exportdefaultcreatePlugin({mcps: (context)=>({"my-mcp": {type: "inline",config: ()=>{constserver=newFastMCP({name: "my-mcp",version: "1.0.0"});server.addTool({name: "get_other_plugin_data",parameters: z.object({}),execute: async()=>{// Access another plugin's contextconstotherPlugin=context.getPlugin("other-plugin");if(!otherPlugin)return"Other plugin not loaded";// Read its stateconstdata=otherPlugin.state.get("shared-data");returnJSON.stringify(data);},});returnserver;},},}),});

Plugin Dependencies

Plugins can declare dependencies on other plugins in plugin.json:

{
"name": "my-plugin",
"version": "1.0.0",
"description": "Depends on base-plugin",
"dependencies": ["base-plugin", "utility-plugin"]
}

Dependencies are loaded first, ensuring they're available when your plugin loads.

Full plugin.json Schema

{
"name": "my-plugin",
"version": "1.0.0",
"description": "A complete example plugin",
"author": "Your Name",
"license": "MIT",
"homepage": "https://example.com/my-plugin",
"repository": "https://github.com/user/my-plugin",
"dependencies": ["other-plugin"]
}

Note: Settings are defined via Zod schema in index.ts, not in plugin.json.

Example: Complete Plugin

Here's a complete example of a plugin with commands, an MCP, and hooks:

// config/plugins/task-tracker/index.tsimport{FastMCP}from"fastmcp";import{z}from"zod";import{createPlugin}from"@/config/helpers";import{createHook}from"@/hooks/hook-generator";exportdefaultcreatePlugin({commands: ()=>({"track": {content: `# Task TrackerTrack a new task: "$ARGUMENTS"Use the task_add MCP tool to add this task. `.trim(),mode: "override",},}),mcps: (context)=>({"task-tracker": {type: "inline",config: ()=>{constserver=newFastMCP({name: "task-tracker",version: "1.0.0",});server.addTool({name: "task_add",description: "Add a new task",parameters: z.object({title: z.string(),priority: z.enum(["low","medium","high"]).default("medium"),}),execute: async(args)=>{consttasks=context.state.get<string[]>("tasks")??[];tasks.push(`[${args.priority}] ${args.title}`);context.state.set("tasks",tasks);return`Added: ${args.title}`;},});server.addTool({name: "task_list",description: "List all tasks",parameters: z.object({}),execute: async()=>{consttasks=context.state.get<string[]>("tasks")??[];returntasks.length>0 ? tasks.join("\n") : "No tasks";},});returnserver;},},}),hooks: ()=>({SessionStart: [{hooks: [createHook({event: "SessionStart",id: "task-tracker-init",handler: ()=>{console.log("📋 Task Tracker plugin loaded");},}),],},],}),});

Viewing Plugin Info

Use ccc --print-config to see loaded CCC plugins:

CCC Plugins:
my-plugin (v1.0.0) [enabled]
Commands: my-plugin:my-command
MCPs: my-plugin:my-mcp
Hooks: Stop(1)

Note: Plugin components are namespaced with the plugin name (e.g., my-plugin:my-command).

Runtime Patches

All CLI patches are applied at runtime - the original node_modules files are never modified. The launcher reads the CLI, applies patches, writes to a temp file, and imports that instead.

Built-in Patches

Applied automatically on every launch:

  • Disable pr-comments and security-review features

User-defined Patches

Add custom string replacements via settings:

exportdefaultcreateConfigSettings({patches: [{find: "ultrathink",replace: "uuu"},// shorter alias],});

Patches are applied after built-in patches. No reinstall needed when changing configuration.

Statusline

Customize the Claude statusline with a simple configuration-based approach.

Priority Order

  1. config/global/statusline.ts - If this file exists, it will be executed with bun
  2. settings.statusLine - Otherwise, use the statusLine configuration from settings
  3. None - If neither is configured, no statusline is displayed

Creating a Statusline

Create config/global/statusline.ts:

import{createStatusline}from"@/config/helpers";importtype{StatusLineInput}from"@/types/statusline";exportdefaultcreateStatusline(async(data: StatusLineInput)=>{constmodelIcon=data.model?.id?.includes("opus") ? "🦆" : "🐇";constcomponents=[];// Model and iconcomponents.push(`${modelIcon}${data.model.display_name}`);// Working directoryif(data.workspace){constdir=data.workspace.project_dir||data.workspace.current_dir;constshortDir=dir.split("/").slice(-2).join("/");components.push(`📁 ${shortDir}`);}// Hook event (if present)if(data.hook_event_name){components.push(`⚡ ${data.hook_event_name}`);}console.log(components.join(" │ "));});

Using External Tools

You can also integrate external statusline tools:

import{createStatusline}from"@/config/helpers";import{$}from"bun";exportdefaultcreateStatusline(async(data)=>{// Use external ccstatusline toolconstoutput=await$`echo ${JSON.stringify(data)} | bunx ccstatusline`.text();constmodelIcon=data.model?.id?.includes("opus") ? "🦆" : "🐇";console.log(`${modelIcon}${output.trim()}`);});

StatusLineInput Type

The statusline function receives a StatusLineInput object with:

  • model.id - Model identifier (e.g., "claude-3-opus-20240229")
  • model.display_name - Human-readable model name
  • workspace.current_dir - Current working directory
  • workspace.project_dir - Project root directory
  • hook_event_name - Current hook event being executed
  • session_id - Current session identifier
  • transcript_path - Path to the transcript file
  • cwd - Current working directory
  • output_style - Output style configuration

Settings Configuration

Alternatively, configure a custom statusline command in settings.ts:

exportdefaultcreateConfigSettings({statusLine: {type: "command",command: "/path/to/your/statusline-script",},});

Note: Unlike other configuration types, statuslines do NOT support layering or merging. Only the global configuration or settings are used.

Doctor (Config Inspector)

Use ccc --doctor to print a diagnostic report of your merged configuration without launching Claude:

ccc --doctor
ccc --doctor --json

The report shows:

  • Presets detected and project configuration in use
  • Layering traces (override/append) for system/user prompts
  • Per-command and per-agent layering traces across global/presets/project
  • MCP servers and their transport type

Dump Configuration

Use ccc --dump-config to create a complete dump of the computed configuration that Claude sees:

ccc --dump-config

This creates a .config-dump/{timestamp}/ directory containing:

  • system.md - The actual computed system prompt
  • user.md - The actual computed user prompt
  • commands/ - All command files as Claude sees them
  • agents/ - All agent files as Claude sees them
  • settings.json - The merged settings
  • mcps.json - The computed MCP configurations
  • metadata.json - Context and dump information

This is useful for debugging configuration issues and understanding exactly what Claude sees.

Debug MCPs

Use ccc --debug-mcp <mcp-name> to launch the MCP Inspector for debugging MCP servers:

ccc --debug-mcp filesystem
ccc --debug-mcp custom-tools

This launches the MCP Inspector with your MCP server, allowing you to:

  • View all available tools, resources, and prompts
  • Test tool invocations interactively
  • Inspect request/response payloads
  • Debug filtered MCPs (shows tools after filtering)

Note:

  • Only works with stdio transport MCPs (not HTTP/SSE)
  • Filtered MCPs will show the filtered tools, not the original ones
  • Inline MCPs (created with FastMCP) are supported

Project Configuration

Create a project-specific configuration:

// config/projects/myapp/project.tsexportdefault{name: "myapp",root: "/path/to/myapp",disableParentClaudeMds: false,// optional, will disable Claude's behavior of loading upper CLAUDE.md files};
// config/projects/myapp/settings.tsimport{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({env: {NODE_ENV: "development",API_URL: "http://localhost:3000",},});

Context Object

All dynamic configurations receive a context object with a few utilities:

{
workingDirectory: string;// Current working directory
launcherDirectory: string;// Path to launcher installation
instanceId: string;// Unique instance identifier
project: Project;// Project instance with config
mcpServers?: Record<string,ClaudeMCPConfig>;// Processed MCP configs for this runisGitRepo(): boolean;// Check if in git repositorygetGitBranch(): string;// Current git branchgetGitStatus(): string;// Git status (porcelain)getGitRecentCommits(n): string;// Recent commit historygetDirectoryTree(): string;// Directory structuregetPlatform(): string;// OS platformgetOsVersion(): string;// OS version infogetCurrentDateTime(): string;// ISO timestamphasMCP(name: string): boolean;// True if MCP with name is configured}

Other things

  • ?

License

MIT License. See LICENSE for details.

About

Custom launcher for Claude Code, supporting dynamic prompts, layered configuration and easy custom hooks and MCPs.

Topics

Resources

Stars

19 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

image

CCC is a launcher for Claude Code that lets you configure prompts, commands, agents, hooks, and MCPs from a single place, in a layered way.

What you get

  • Dynamic Configuration: Generate system/user prompts, commands, agents dynamically.
  • Layered config: Merge global configuration with presets and project overrides.
  • Low‑effort extensibility: write hooks & MCPs in TypeScript with tiny helpers.
image

Not affiliated with Anthropic. Uses the official @anthropic-ai/claude-code CLI.

Warning: Not tested on Windows, open an issue if you run into problems.


Getting Started

1. Setup

# clone this repo somewhere you'd like to keep going to edit your configuration
git clone https://github.com/3rd/ccc.git ~/my-claude-launcher
cd~/my-claude-launcher
# install dependencies and link `ccc`
bun install
bun link
# install tsx globally (required for runtime interception)
bun add -g tsx

Note: Claude Code (@anthropic-ai/claude-code) is included as a dependency.
Bun must also be available on PATH at runtime because CCC launches hook and statusline helpers with bun.
To update it to the latest version do a bun update.

2. Customize your config

Your configuration lives in the ./config directory, which includes some examples by default.

~/my-claude-launcher/ # Your copy of this repository
└── config/
├── global/ # Global configuration
│ ├── prompts/ # System (output style) / user (CLAUDE.md) prompts
│ ├── commands/ # Your commands
│ ├── agents/ # Your sub-agents
│ ├── skills/ # Your skills
│ ├── hooks.ts # Your hooks
│ └── mcps.ts # Your MCPs
├── presets/ # Your language/framework/whatever-specific configs
│ └── typescript/ # Example: TypeScript-specific settings
└── projects/ # Your project-specific overrides
└── myapp/ # Example: Settings for your 'myapp' project

Development Mode: If a ./dev-config directory exists, it will be used instead of ./config. This allows you to keep the example configuration in ./config (committed to git) while using ./dev-config for your actual development configuration.

3. Use it

Your workflow:

  1. Edit your config in ~/my-claude-launcher/config/
  2. Run ccc instead of claude from anywhere
  3. Your config is dynamically built and loaded
ccc # wrap and launch claude
ccc --continue # all the arguments you pass will be passed through to claude# except these special cases used for debugging
ccc --doctor
ccc --print-config
ccc --print-system-prompt
ccc --print-user-prompt
ccc --dump-config
ccc --debug-mcp <mcp-name>
ccc --doru # launcher-only flag; place it before Claude args
ccc --doru --continue

ccc --doru runs CCC through npx doru --ui and auto-opens doru's live UI. Treat --doru as a launcher-only leading flag and place it before any Claude args. The first run may download doru, and doru itself requires npx plus Node.js 22+.

Configuration Layers

ccc loads configurations in layers (later overrides earlier):

  1. Globalconfig/global/ - Base configuration for all projects
  2. Presetsconfig/presets/ - Auto-detected based on project type
  3. Projectsconfig/projects/ - Specific project overrides

Each layer can define:

  • settings.ts - Settings that will go into Claude Code's settings.json
  • prompts/user.{md,ts} - User instructions (CLAUDE.md)
  • prompts/system.{md,ts} - Output style
  • commands/*.{md,ts} - Custom slash commands
  • agents/*.{md,ts} - Custom sub-agents
  • skills/*/SKILL.{md,ts} - Custom skills (and supporting files)
  • hooks.ts - Custom hooks
  • mcps.ts - Custom MCPs

How It Works

ccc injects configurations using a virtual filesystem overlay. Your actual Claude installation remains untouched. Configurations are injected at runtime through Node.js module interception.

The launcher:

  1. Discovers and merges configurations from all layers
  2. Generates a vfs with the merged config
  3. Intercepts Node's modules to serve virtual files
  4. Launches Claude with the injected configuration
Global ┐
Preset ├─► merge ─► "virtual overlay" ─► Claude Code
Project ┘

Using Models from Other Vendors

You can configure CCC to use models from other vendors by setting environment variables in your settings.ts. This allows you to use models like GLM, Kimi K2, or Deepseek through their Anthropic-compatible APIs.

Configuration Examples

Add these environment variables to your config/global/settings.ts:

import{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({env: {// GLM 4.5ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",ANTHROPIC_AUTH_TOKEN: "Z_API_KEY",ANTHROPIC_MODEL: "glm-4.5",ANTHROPIC_FAST_MODEL: "glm-4.5-air",// Kimi K2// ANTHROPIC_BASE_URL: "https://api.moonshot.ai/anthropic",// ANTHROPIC_AUTH_TOKEN: "KIMI_API_KEY",// Deepseek// ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",// ANTHROPIC_AUTH_TOKEN: "DEEPSEEK_API_KEY",// ANTHROPIC_MODEL: "deepseek-chat",// ANTHROPIC_FAST_MODEL: "deepseek-chat"}});

Environment Variables

  • ANTHROPIC_BASE_URL - The base URL for the vendor's API endpoint
  • ANTHROPIC_AUTH_TOKEN - Your API key for the vendor
  • ANTHROPIC_MODEL - The main model to use (e.g., "glm-4.5", "deepseek-chat")
  • ANTHROPIC_FAST_MODEL - The model to use for quick operations (optional)

Usage

Once configured, CCC will automatically use the specified vendor's models instead of Anthropic's models. All CCC features like prompts, commands, agents, hooks, and MCPs will continue to work with the alternative models.

Note: Make sure you have the required API keys and that the vendor's API is compatible with Anthropic's API format.

Extra Configuration

Some settings will still be read from your global ~/.claude.json:

# things like these:
claude config set -g autocheckpointingEnabled true
claude config set -g diffTool delta
claude config set -g supervisorMode true
claude config set -g autoCompactEnabled true
claude config set --global preferredNotifChannel terminal_bell
claude config set -g verbose true

CLI argument settings

Some settings are passed directly as CLI arguments to Claude rather than being written to settings.json. These are grouped under settings.cli. See Claude Code CLI Reference for details.

// config/global/settings.tsimport{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({cli: {tools: ["Bash","Read","Edit","Write"],disallowedTools: ["WebSearch","WebFetch"],allowedTools: ["Read","Glob","Grep"],addDir: ["/path/to/shared/libs"],permissionMode: "plan",verbose: true,debug: "api,hooks",chrome: true,ide: true,enableLspLogging: false,agent: "code-reviewer",},// other settings go to settings.json as normalenv: { ... },});

Available CLI-Only Settings

SettingTypeCLI FlagDescription
toolsstring[] | "default"--tools "Tool1,Tool2"Available tools ("default" for all, [] to disable)
disallowedToolsstring[]--disallowedTools "Tool1,Tool2"Tools removed from model context entirely
allowedToolsstring[]--allowedTools "Tool1,Tool2"Tools that execute without permission prompts
addDirstring[]--add-dir pathAdditional directories Claude can access
permissionModeenum--permission-mode modeStart mode: default, acceptEdits, plan, bypassPermissions
verboseboolean--verboseEnable verbose logging
debugboolean | string--debug [filter]Enable debug mode, optionally with category filter
chromeboolean--chrome / --no-chromeEnable/disable Chrome browser integration
ideboolean--ideAuto-connect to IDE on startup
enableLspLoggingboolean--enable-lsp-loggingEnable verbose LSP logging
agentstring--agent nameDefault agent for the session
agentsRecord<string, AgentDef>--agents JSONCustom subagent definitions
forkSessionboolean--fork-sessionCreate new session ID when resuming
fallbackModelstring--fallback-model nameFallback model when primary is overloaded
settingSourcesstring[]--setting-sources "user,project,local"Config sources to use
strictMcpConfigboolean--strict-mcp-configOnly use specified MCP config

CLI Override

CLI arguments provided directly to ccc override the corresponding settings.cli values:

# override disallowedTools from settings
ccc --disallowedTools "WebSearch,WebFetch"# start in plan mode regardless of settings
ccc --permission-mode plan

System & User Prompts

System Prompt (Output Style)

Controls how Claude responds and behaves.

Static (Markdown) (config/global/prompts/system.md):

You are a helpful coding assistant.
Write clean, maintainable code.
Follow best practices.

Dynamic (TypeScript) (config/global/prompts/system.ts):

import{createPrompt}from"@/config/helpers";exportdefaultcreatePrompt((context)=>`You are working in ${context.workingDirectory}${context.isGitRepo() ? `Current branch: ${context.getGitBranch()}` : ""}Write clean, maintainable code.`,);

Append Mode (adds to previous layers):

import{createAppendPrompt}from"@/config/helpers";exportdefaultcreateAppendPrompt((context)=>`Additional instructions for this preset.`,);

You can also use Markdown files in append mode, just name them: <target>.append.md

User Prompt (CLAUDE.md)

Project-specific instructions and context. See config/global/prompts/user.ts for a full example:

import{createPrompt}from"@/config/helpers";exportdefaultcreatePrompt((context)=>`# CRITICAL RULESDo exactly what the user asks. No alternatives, no "better" solutions...Working in: ${context.workingDirectory}Git branch: ${context.getGitBranch()}`,);

Commands

Custom slash commands available in Claude. See config/global/commands/ for examples:

Static (Markdown) (config/global/commands/review.md):

# Review
Review: "$ARGUMENTS"
You are conducting a code review...

Dynamic (TypeScript):

import{createCommand}from"@/config/helpers";exportdefaultcreateCommand((context)=>`# Custom CommandWorking in ${context.workingDirectory}Current branch: ${context.getGitBranch()}Your command instructions here...`,);

Append to existing command:

import{createAppendCommand}from"@/config/helpers";exportdefaultcreateAppendCommand((context)=>`Additional instructions for TypeScript projects...`,);

Skills

Skills are reusable instruction bundles that Claude can invoke via the Skill tool. Define them as folders under skills/ with either SKILL.md (static) or SKILL.ts (structured) and any supporting files (e.g. references/*.md).

Static (Markdown) (config/global/skills/my-skill/SKILL.md):

---name: my-skilldescription: Quick checks for the current repoallowed-tools:
- Read
- Grep---
Use this skill to run quick repository checks and summarize findings.

Structured (TypeScript) (config/global/skills/my-skill/SKILL.ts):

import{createSkill}from"@/config/helpers";exportdefaultcreateSkill((context)=>({description: `Checks for ${context.project.name}`,content: `Run targeted analysis for ${context.workingDirectory}.Use $ARGUMENTS to accept parameters.`,allowedTools: ["Read","Grep"],userInvocable: true,disableModelInvocation: false,hooks: {PreToolUse: [{matcher: "Bash",hooks: [{type: "command",command: "echo 'Skill hook'"}],},],},}));

Skill layers are resolved global, then preset, then project. A structured skill may set mode: "append" to append its SKILL.md body to an earlier skill with the same name while keeping the base skill metadata; omitted mode means override.

Hooks

Event handlers that run at specific Claude events. See config/global/hooks.ts for examples:

Examples for global hooks:

importpfrom"picocolors";import{createHook}from"@/hooks/hook-generator";import{createConfigHooks}from"@/config/helpers";constbashDenyList=[{match: /^\bgit\bcheckout/,message: "You are not allowed to do checkouts or resets",},{match: /^\bgrep\b(?!.*\|)/,message: "Use 'rg' (ripgrep) instead of 'grep' for better performance",},];constsessionStartHook=createHook({event: "SessionStart",id: "global-session-start",handler: (input)=>{consttimestamp=newDate().toISOString();console.log(p.dim("🞄"));console.log(`🚀 Session started from ${p.yellow(input.source)} at ${p.blue(timestamp)}`,);console.log(`📍 Working directory: ${p.yellow(process.cwd())}`);console.log(`🔧 Node version: ${p.yellow(process.version)}`);console.log(p.dim("🞄"));},});constpreBashValidationHook=createHook({event: "PreToolUse",id: "bash-deny-list",handler: (input)=>{constcommand=input.tool_input.commandasstring;if(input.tool_name!=="Bash"||!command)return;constfirstMatchingRule=bashDenyList.find((rule)=>command.match(rule.match),);if(!firstMatchingRule)return;return{continue: true,decision: "block",reason: firstMatchingRule?.message,};},});exportdefaultcreateConfigHooks({SessionStart: [{hooks: [sessionStartHook]}],PreToolUse: [{hooks: [preBashValidationHook]}],});

Hook Options:

constoneTimeHook=createHook({event: "SessionStart",id: "one-time-setup",handler: (input)=>{// this will only run once per sessionconsole.log("First session start only");},timeout: 5,// optional: timeout in secondsonce: true,// optional: run only first time per session});

TypeScript Validation Example (config/presets/typescript/hooks.ts):

import{$}from"zx";import{createHook}from"@/hooks/hook-generator";import{createConfigHooks}from"@/config/helpers";exportdefaultcreateConfigHooks({Stop: [{hooks: [createHook({event: "Stop",id: "typescript-validation",handler: async()=>{constresult=await$`tsc --noEmit`;if(result.exitCode!==0){return{continue: true,decision: "block",reason: `Failed tsc --noEmit:\n${result.text()}`,};}return{suppressOutput: true};},}),],},],});

Agents

Specialized sub-agents for specific tasks. See config/global/agents/ for examples:

Static (Markdown) (config/global/agents/code-reviewer.md):

---name: code-reviewerdescription: Reviews code for quality and best practicestools: [Read, Grep, Glob, Bash]---# Code Reviewer Agent
You are a specialized code review agent conducting **SYSTEMATIC, EVIDENCE-FIRST CODE REVIEWS**.
## Core Principles**EVIDENCE BEFORE OPINION** - Always provide file:line references...

Dynamic (TypeScript):

import{createAgent}from"@/config/helpers";exportdefaultcreateAgent((context)=>`---name: debuggerdescription: Debug issues in ${context.project.name}tools: [Read, Edit, Bash, Grep, Glob]---# Debugger AgentYou are debugging code in ${context.workingDirectory}Current branch: ${context.getGitBranch()}`,);

MCPs

Model Context Protocol servers for extending Claude's capabilities. See config/global/mcps/ for examples:

External MCPs

import{createConfigMCPs}from"@/config/helpers";exportdefaultcreateConfigMCPs({filesystem: {command: "npx",args: ["@modelcontextprotocol/server-filesystem"],env: {FS_ROOT: "/home/user"},},});

Filtering MCP Tools

You can filter which tools are exposed from an external MCP:

import{createConfigMCPs}from"@/config/helpers";exportdefaultcreateConfigMCPs({nixos: {command: "nix",args: ["run","github:utensils/mcp-nixos","--"],filter: (tool)=>{// Exclude specific toolsreturntool.name!=="nixos_search";},},});

The filter function receives a tool object with name and description properties. Return true to include the tool, false to exclude it.

Custom MCPs

You can easily define custom MCPs in your config using FastMCP.

import{FastMCP}from"fastmcp";import{z}from"zod";import{createConfigMCPs,createMCP}from"@/config/helpers";constcustomTools=createMCP((context)=>{constserver=newFastMCP({name: "custom-tools",version: "1.0.0",});server.addTool({name: "getProjectInfo",description: "Get current project information",parameters: z.object({}),execute: async()=>{returnJSON.stringify({directory: context.workingDirectory,branch: context.getGitBranch(),isGitRepo: context.isGitRepo(),},null,2,);},});returnserver;});exportdefaultcreateConfigMCPs({"custom-tools": customTools,});

Plugins

CCC configures Claude Code plugins via layered plugins.ts files (global/preset/project). Claude plugin settings live under the claude namespace.

Workflow

  1. Install plugins using the /plugin command
  2. Find plugin keys in ~/.claude/plugins/installed_plugins.json
  3. Enable plugins in your plugins.ts layer

Enabling Claude Plugins

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({claude: {enabledPlugins: {// Use keys from ~/.claude/plugins/installed_plugins.json"typescript-lsp@claude-plugins-official": true,"gopls-lsp@claude-plugins-official": true,},},});

Local Plugin Directories

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({claude: {pluginDirs: ["./claude-plugins/my-plugin",],},});

You can also drop local plugins into config/claude-plugins/ and CCC will auto-discover them. CLI override: ccc --plugin-dir ./path/to/plugin

LSP Plugin Support

LSP plugins are supported natively. Just enable your LSP plugins normally:

exportdefaultcreateConfigPlugins({claude: {enabledPlugins: {"typescript-lsp@claude-plugins-official": true,},},});

CCC Plugins

CCC has its own plugin system for bundling reusable configuration components. Unlike Claude's built-in plugins (configured via plugins.ts under claude), CCC plugins are local TypeScript modules that can define commands, agents, MCPs, hooks, and prompts dynamically, and which have access to enriched information about the current session.

CCC Plugins vs Claude Plugins

AspectCCC Plugins (plugins.tsccc)Claude Plugins (plugins.tsclaude)
Locationconfig/plugins/ directory~/.claude/plugins/ (installed) or local roots via config/claude-plugins/
FormatTypeScript with createPlugin()Claude's plugin format
Componentscommands, agents, MCPs, hooks, promptsDefined by Claude
DistributionLocal to your configVia /plugin command

Plugin Structure

A CCC plugin requires two files:

config/plugins/my-plugin/
├── plugin.json # Manifest with name, version, description
└── index.ts # Plugin definition using createPlugin()

plugin.json (manifest):

{
"name": "my-plugin",
"version": "1.0.0",
"description": "A custom CCC plugin"
}

index.ts (definition):

import{createPlugin}from"@/config/helpers";exportdefaultcreatePlugin({// Commands - custom slash commandscommands: (context)=>({"my-command": {content: `# My CommandYour command instructions here... `.trim(),mode: "override",},}),// Agents - specialized sub-agentsagents: (context)=>({"my-agent": {content: `---name: my-agentdescription: A specialized agenttools: [Read, Grep, Glob]---Agent instructions... `.trim(),mode: "override",},}),// MCPs - inline MCP serversmcps: (context)=>({"my-mcp": {type: "inline",config: ()=>createMyMCP(context),},}),// Hooks - event handlershooks: (context)=>({Stop: [{hooks: [createHook({event: "Stop",id: "plugin-stop-hook",handler: ()=>{// Hook logic},}),],},],}),// Prompts - system/user prompt additionsprompts: (context)=>({user: {content: "Additional user prompt content",mode: "append",},}),});

Enabling CCC Plugins

Enable CCC plugins via plugins.ts:

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"my-plugin": true,// Enable plugin"another-plugin": false,// Explicitly disable},});

You can also enable plugins in presets:

// config/presets/typescript/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"typescript-helpers": true,},});

Plugin Context

Plugins receive a PluginContext with access to:

{// Standard context properties
workingDirectory: string;
launcherDirectory: string;isGitRepo(): boolean;getGitBranch(): string;// ... all Context methods// Plugin-specific
manifest: PluginManifest;// Plugin's plugin.json
root: string;// Plugin directory path
settings: Record<string,unknown>;// Plugin settings// State management
state: {get<T>(key: string): T|undefined;set(key: string,value: unknown): void;clear(): void;getAll(): Record<string,unknown>;};}

Plugin State

Plugins can persist state using the built-in state API. State is automatically saved to disk and restored between sessions.

State API:

context.state.get<T>(key: string): T|undefined// Get a valuecontext.state.set(key: string,value: unknown): void// Set a valuecontext.state.clear(): void// Clear all statecontext.state.getAll(): Record<string,unknown>// Get all state

State Locations:

By default, plugin state is stored in /tmp/ccc-plugin-{name}-{sessionId}.json. The location is isolated by session ID (CCC_INSTANCE_ID) to prevent conflicts between concurrent CCC instances.

LocationPathUse Case
temp (default)/tmp/ccc-plugin-{name}-{sessionId}.jsonSession-scoped data
project{cwd}/.ccc/state/plugins/{name}.jsonProject-specific persistent data
user~/.ccc/state/plugins/{name}.jsonGlobal user preferences

Example - Stateful MCP:

exportdefaultcreatePlugin({mcps: (context)=>({"stateful-mcp": {type: "inline",config: ()=>{constserver=newFastMCP({name: "stateful",version: "1.0.0"});server.addTool({name: "save_data",parameters: z.object({key: z.string(),value: z.string()}),execute: async(args)=>{context.state.set(args.key,args.value);return"Saved!";},});server.addTool({name: "load_data",parameters: z.object({key: z.string()}),execute: async(args)=>{returncontext.state.get(args.key)??"Not found";},});returnserver;},},}),});

Plugin Settings

Plugins define settings using Zod schemas in index.ts. The type is automatically inferred:

// my-plugin/index.tsimport{z}from"zod";import{createPlugin}from"@/config/helpers";constsettingsSchema=z.object({maxItems: z.number().default(100),mode: z.enum(["fast","balanced","thorough"]).default("balanced"),});exportdefaultcreatePlugin({
settingsSchema,mcps: (context)=>{// context.plugin.settings is typed as { maxItems: number, mode: "fast" | "balanced" | "thorough" }const{ maxItems, mode }=context.plugin.settings;// ...},});

Pass settings when enabling the plugin:

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"my-plugin": {enabled: true,settings: {maxItems: 50,mode: "fast",},},},});

Settings are validated with Zod at plugin load time. Invalid settings throw errors.

Plugin State

Plugins get a context.state API for key-value storage:

context.state.get<T>(key)// get valuecontext.state.set(key,value)// set valuecontext.state.clear()// clear all statecontext.state.getAll()// get all state

By default, state is in-memory only (not persisted). To persist state, set stateType:

exportdefaultcreatePlugin({stateType: "temp",// /tmp/ccc-plugin-{name}-{sessionId}.json (per-instance)// or:stateType: "project",// {projectRoot}/.ccc/state/plugins/{name}.json// or:stateType: "user",// ~/.ccc/state/plugins/{name}.jsonmcps: (context)=>{/* ... */},});
stateTypePathUse case
"none" (default)(in-memory only)No persistence needed
"temp"/tmp/ccc-plugin-{name}-{sessionId}.jsonPer-instance state, cleared on reboot
"project"{projectRoot}/.ccc/state/plugins/{name}.jsonProject-specific persistent state
"user"~/.ccc/state/plugins/{name}.jsonUser-wide persistent state

onLoad Hook

Plugins can define an onLoad callback for initialization:

exportdefaultcreatePlugin({onLoad: async(context)=>{console.log(`Plugin ${context.plugin.name} loaded`);},commands: ()=>({/* ... */}),});

Inter-Plugin Communication

Plugins can access other loaded plugins using getPlugin():

exportdefaultcreatePlugin({mcps: (context)=>({"my-mcp": {type: "inline",config: ()=>{constserver=newFastMCP({name: "my-mcp",version: "1.0.0"});server.addTool({name: "get_other_plugin_data",parameters: z.object({}),execute: async()=>{// Access another plugin's contextconstotherPlugin=context.getPlugin("other-plugin");if(!otherPlugin)return"Other plugin not loaded";// Read its stateconstdata=otherPlugin.state.get("shared-data");returnJSON.stringify(data);},});returnserver;},},}),});

Plugin Dependencies

Plugins can declare dependencies on other plugins in plugin.json:

{
"name": "my-plugin",
"version": "1.0.0",
"description": "Depends on base-plugin",
"dependencies": ["base-plugin", "utility-plugin"]
}

Dependencies are loaded first, ensuring they're available when your plugin loads.

Full plugin.json Schema

{
"name": "my-plugin",
"version": "1.0.0",
"description": "A complete example plugin",
"author": "Your Name",
"license": "MIT",
"homepage": "https://example.com/my-plugin",
"repository": "https://github.com/user/my-plugin",
"dependencies": ["other-plugin"]
}

Note: Settings are defined via Zod schema in index.ts, not in plugin.json.

Example: Complete Plugin

Here's a complete example of a plugin with commands, an MCP, and hooks:

// config/plugins/task-tracker/index.tsimport{FastMCP}from"fastmcp";import{z}from"zod";import{createPlugin}from"@/config/helpers";import{createHook}from"@/hooks/hook-generator";exportdefaultcreatePlugin({commands: ()=>({"track": {content: `# Task TrackerTrack a new task: "$ARGUMENTS"Use the task_add MCP tool to add this task. `.trim(),mode: "override",},}),mcps: (context)=>({"task-tracker": {type: "inline",config: ()=>{constserver=newFastMCP({name: "task-tracker",version: "1.0.0",});server.addTool({name: "task_add",description: "Add a new task",parameters: z.object({title: z.string(),priority: z.enum(["low","medium","high"]).default("medium"),}),execute: async(args)=>{consttasks=context.state.get<string[]>("tasks")??[];tasks.push(`[${args.priority}] ${args.title}`);context.state.set("tasks",tasks);return`Added: ${args.title}`;},});server.addTool({name: "task_list",description: "List all tasks",parameters: z.object({}),execute: async()=>{consttasks=context.state.get<string[]>("tasks")??[];returntasks.length>0 ? tasks.join("\n") : "No tasks";},});returnserver;},},}),hooks: ()=>({SessionStart: [{hooks: [createHook({event: "SessionStart",id: "task-tracker-init",handler: ()=>{console.log("📋 Task Tracker plugin loaded");},}),],},],}),});

Viewing Plugin Info

Use ccc --print-config to see loaded CCC plugins:

CCC Plugins:
my-plugin (v1.0.0) [enabled]
Commands: my-plugin:my-command
MCPs: my-plugin:my-mcp
Hooks: Stop(1)

Note: Plugin components are namespaced with the plugin name (e.g., my-plugin:my-command).

Runtime Patches

All CLI patches are applied at runtime - the original node_modules files are never modified. The launcher reads the CLI, applies patches, writes to a temp file, and imports that instead.

Built-in Patches

Applied automatically on every launch:

  • Disable pr-comments and security-review features

User-defined Patches

Add custom string replacements via settings:

exportdefaultcreateConfigSettings({patches: [{find: "ultrathink",replace: "uuu"},// shorter alias],});

Patches are applied after built-in patches. No reinstall needed when changing configuration.

Statusline

Customize the Claude statusline with a simple configuration-based approach.

Priority Order

  1. config/global/statusline.ts - If this file exists, it will be executed with bun
  2. settings.statusLine - Otherwise, use the statusLine configuration from settings
  3. None - If neither is configured, no statusline is displayed

Creating a Statusline

Create config/global/statusline.ts:

import{createStatusline}from"@/config/helpers";importtype{StatusLineInput}from"@/types/statusline";exportdefaultcreateStatusline(async(data: StatusLineInput)=>{constmodelIcon=data.model?.id?.includes("opus") ? "🦆" : "🐇";constcomponents=[];// Model and iconcomponents.push(`${modelIcon}${data.model.display_name}`);// Working directoryif(data.workspace){constdir=data.workspace.project_dir||data.workspace.current_dir;constshortDir=dir.split("/").slice(-2).join("/");components.push(`📁 ${shortDir}`);}// Hook event (if present)if(data.hook_event_name){components.push(`⚡ ${data.hook_event_name}`);}console.log(components.join(" │ "));});

Using External Tools

You can also integrate external statusline tools:

import{createStatusline}from"@/config/helpers";import{$}from"bun";exportdefaultcreateStatusline(async(data)=>{// Use external ccstatusline toolconstoutput=await$`echo ${JSON.stringify(data)} | bunx ccstatusline`.text();constmodelIcon=data.model?.id?.includes("opus") ? "🦆" : "🐇";console.log(`${modelIcon}${output.trim()}`);});

StatusLineInput Type

The statusline function receives a StatusLineInput object with:

  • model.id - Model identifier (e.g., "claude-3-opus-20240229")
  • model.display_name - Human-readable model name
  • workspace.current_dir - Current working directory
  • workspace.project_dir - Project root directory
  • hook_event_name - Current hook event being executed
  • session_id - Current session identifier
  • transcript_path - Path to the transcript file
  • cwd - Current working directory
  • output_style - Output style configuration

Settings Configuration

Alternatively, configure a custom statusline command in settings.ts:

exportdefaultcreateConfigSettings({statusLine: {type: "command",command: "/path/to/your/statusline-script",},});

Note: Unlike other configuration types, statuslines do NOT support layering or merging. Only the global configuration or settings are used.

Doctor (Config Inspector)

Use ccc --doctor to print a diagnostic report of your merged configuration without launching Claude:

ccc --doctor
ccc --doctor --json

The report shows:

  • Presets detected and project configuration in use
  • Layering traces (override/append) for system/user prompts
  • Per-command and per-agent layering traces across global/presets/project
  • MCP servers and their transport type

Dump Configuration

Use ccc --dump-config to create a complete dump of the computed configuration that Claude sees:

ccc --dump-config

This creates a .config-dump/{timestamp}/ directory containing:

  • system.md - The actual computed system prompt
  • user.md - The actual computed user prompt
  • commands/ - All command files as Claude sees them
  • agents/ - All agent files as Claude sees them
  • settings.json - The merged settings
  • mcps.json - The computed MCP configurations
  • metadata.json - Context and dump information

This is useful for debugging configuration issues and understanding exactly what Claude sees.

Debug MCPs

Use ccc --debug-mcp <mcp-name> to launch the MCP Inspector for debugging MCP servers:

ccc --debug-mcp filesystem
ccc --debug-mcp custom-tools

This launches the MCP Inspector with your MCP server, allowing you to:

  • View all available tools, resources, and prompts
  • Test tool invocations interactively
  • Inspect request/response payloads
  • Debug filtered MCPs (shows tools after filtering)

Note:

  • Only works with stdio transport MCPs (not HTTP/SSE)
  • Filtered MCPs will show the filtered tools, not the original ones
  • Inline MCPs (created with FastMCP) are supported

Project Configuration

Create a project-specific configuration:

// config/projects/myapp/project.tsexportdefault{name: "myapp",root: "/path/to/myapp",disableParentClaudeMds: false,// optional, will disable Claude's behavior of loading upper CLAUDE.md files};
// config/projects/myapp/settings.tsimport{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({env: {NODE_ENV: "development",API_URL: "http://localhost:3000",},});

Context Object

All dynamic configurations receive a context object with a few utilities:

{
workingDirectory: string;// Current working directory
launcherDirectory: string;// Path to launcher installation
instanceId: string;// Unique instance identifier
project: Project;// Project instance with config
mcpServers?: Record<string,ClaudeMCPConfig>;// Processed MCP configs for this runisGitRepo(): boolean;// Check if in git repositorygetGitBranch(): string;// Current git branchgetGitStatus(): string;// Git status (porcelain)getGitRecentCommits(n): string;// Recent commit historygetDirectoryTree(): string;// Directory structuregetPlatform(): string;// OS platformgetOsVersion(): string;// OS version infogetCurrentDateTime(): string;// ISO timestamphasMCP(name: string): boolean;// True if MCP with name is configured}

Other things

  • ?

License

MIT License. See LICENSE for details.

About

Custom launcher for Claude Code, supporting dynamic prompts, layered configuration and easy custom hooks and MCPs.

Topics

Resources

Stars

19 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

image

CCC is a launcher for Claude Code that lets you configure prompts, commands, agents, hooks, and MCPs from a single place, in a layered way.

What you get

  • Dynamic Configuration: Generate system/user prompts, commands, agents dynamically.
  • Layered config: Merge global configuration with presets and project overrides.
  • Low‑effort extensibility: write hooks & MCPs in TypeScript with tiny helpers.
image

Not affiliated with Anthropic. Uses the official @anthropic-ai/claude-code CLI.

Warning: Not tested on Windows, open an issue if you run into problems.


Getting Started

1. Setup

# clone this repo somewhere you'd like to keep going to edit your configuration
git clone https://github.com/3rd/ccc.git ~/my-claude-launcher
cd~/my-claude-launcher
# install dependencies and link `ccc`
bun install
bun link
# install tsx globally (required for runtime interception)
bun add -g tsx

Note: Claude Code (@anthropic-ai/claude-code) is included as a dependency.
Bun must also be available on PATH at runtime because CCC launches hook and statusline helpers with bun.
To update it to the latest version do a bun update.

2. Customize your config

Your configuration lives in the ./config directory, which includes some examples by default.

~/my-claude-launcher/ # Your copy of this repository
└── config/
├── global/ # Global configuration
│ ├── prompts/ # System (output style) / user (CLAUDE.md) prompts
│ ├── commands/ # Your commands
│ ├── agents/ # Your sub-agents
│ ├── skills/ # Your skills
│ ├── hooks.ts # Your hooks
│ └── mcps.ts # Your MCPs
├── presets/ # Your language/framework/whatever-specific configs
│ └── typescript/ # Example: TypeScript-specific settings
└── projects/ # Your project-specific overrides
└── myapp/ # Example: Settings for your 'myapp' project

Development Mode: If a ./dev-config directory exists, it will be used instead of ./config. This allows you to keep the example configuration in ./config (committed to git) while using ./dev-config for your actual development configuration.

3. Use it

Your workflow:

  1. Edit your config in ~/my-claude-launcher/config/
  2. Run ccc instead of claude from anywhere
  3. Your config is dynamically built and loaded
ccc # wrap and launch claude
ccc --continue # all the arguments you pass will be passed through to claude# except these special cases used for debugging
ccc --doctor
ccc --print-config
ccc --print-system-prompt
ccc --print-user-prompt
ccc --dump-config
ccc --debug-mcp <mcp-name>
ccc --doru # launcher-only flag; place it before Claude args
ccc --doru --continue

ccc --doru runs CCC through npx doru --ui and auto-opens doru's live UI. Treat --doru as a launcher-only leading flag and place it before any Claude args. The first run may download doru, and doru itself requires npx plus Node.js 22+.

Configuration Layers

ccc loads configurations in layers (later overrides earlier):

  1. Globalconfig/global/ - Base configuration for all projects
  2. Presetsconfig/presets/ - Auto-detected based on project type
  3. Projectsconfig/projects/ - Specific project overrides

Each layer can define:

  • settings.ts - Settings that will go into Claude Code's settings.json
  • prompts/user.{md,ts} - User instructions (CLAUDE.md)
  • prompts/system.{md,ts} - Output style
  • commands/*.{md,ts} - Custom slash commands
  • agents/*.{md,ts} - Custom sub-agents
  • skills/*/SKILL.{md,ts} - Custom skills (and supporting files)
  • hooks.ts - Custom hooks
  • mcps.ts - Custom MCPs

How It Works

ccc injects configurations using a virtual filesystem overlay. Your actual Claude installation remains untouched. Configurations are injected at runtime through Node.js module interception.

The launcher:

  1. Discovers and merges configurations from all layers
  2. Generates a vfs with the merged config
  3. Intercepts Node's modules to serve virtual files
  4. Launches Claude with the injected configuration
Global ┐
Preset ├─► merge ─► "virtual overlay" ─► Claude Code
Project ┘

Using Models from Other Vendors

You can configure CCC to use models from other vendors by setting environment variables in your settings.ts. This allows you to use models like GLM, Kimi K2, or Deepseek through their Anthropic-compatible APIs.

Configuration Examples

Add these environment variables to your config/global/settings.ts:

import{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({env: {// GLM 4.5ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",ANTHROPIC_AUTH_TOKEN: "Z_API_KEY",ANTHROPIC_MODEL: "glm-4.5",ANTHROPIC_FAST_MODEL: "glm-4.5-air",// Kimi K2// ANTHROPIC_BASE_URL: "https://api.moonshot.ai/anthropic",// ANTHROPIC_AUTH_TOKEN: "KIMI_API_KEY",// Deepseek// ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",// ANTHROPIC_AUTH_TOKEN: "DEEPSEEK_API_KEY",// ANTHROPIC_MODEL: "deepseek-chat",// ANTHROPIC_FAST_MODEL: "deepseek-chat"}});

Environment Variables

  • ANTHROPIC_BASE_URL - The base URL for the vendor's API endpoint
  • ANTHROPIC_AUTH_TOKEN - Your API key for the vendor
  • ANTHROPIC_MODEL - The main model to use (e.g., "glm-4.5", "deepseek-chat")
  • ANTHROPIC_FAST_MODEL - The model to use for quick operations (optional)

Usage

Once configured, CCC will automatically use the specified vendor's models instead of Anthropic's models. All CCC features like prompts, commands, agents, hooks, and MCPs will continue to work with the alternative models.

Note: Make sure you have the required API keys and that the vendor's API is compatible with Anthropic's API format.

Extra Configuration

Some settings will still be read from your global ~/.claude.json:

# things like these:
claude config set -g autocheckpointingEnabled true
claude config set -g diffTool delta
claude config set -g supervisorMode true
claude config set -g autoCompactEnabled true
claude config set --global preferredNotifChannel terminal_bell
claude config set -g verbose true

CLI argument settings

Some settings are passed directly as CLI arguments to Claude rather than being written to settings.json. These are grouped under settings.cli. See Claude Code CLI Reference for details.

// config/global/settings.tsimport{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({cli: {tools: ["Bash","Read","Edit","Write"],disallowedTools: ["WebSearch","WebFetch"],allowedTools: ["Read","Glob","Grep"],addDir: ["/path/to/shared/libs"],permissionMode: "plan",verbose: true,debug: "api,hooks",chrome: true,ide: true,enableLspLogging: false,agent: "code-reviewer",},// other settings go to settings.json as normalenv: { ... },});

Available CLI-Only Settings

SettingTypeCLI FlagDescription
toolsstring[] | "default"--tools "Tool1,Tool2"Available tools ("default" for all, [] to disable)
disallowedToolsstring[]--disallowedTools "Tool1,Tool2"Tools removed from model context entirely
allowedToolsstring[]--allowedTools "Tool1,Tool2"Tools that execute without permission prompts
addDirstring[]--add-dir pathAdditional directories Claude can access
permissionModeenum--permission-mode modeStart mode: default, acceptEdits, plan, bypassPermissions
verboseboolean--verboseEnable verbose logging
debugboolean | string--debug [filter]Enable debug mode, optionally with category filter
chromeboolean--chrome / --no-chromeEnable/disable Chrome browser integration
ideboolean--ideAuto-connect to IDE on startup
enableLspLoggingboolean--enable-lsp-loggingEnable verbose LSP logging
agentstring--agent nameDefault agent for the session
agentsRecord<string, AgentDef>--agents JSONCustom subagent definitions
forkSessionboolean--fork-sessionCreate new session ID when resuming
fallbackModelstring--fallback-model nameFallback model when primary is overloaded
settingSourcesstring[]--setting-sources "user,project,local"Config sources to use
strictMcpConfigboolean--strict-mcp-configOnly use specified MCP config

CLI Override

CLI arguments provided directly to ccc override the corresponding settings.cli values:

# override disallowedTools from settings
ccc --disallowedTools "WebSearch,WebFetch"# start in plan mode regardless of settings
ccc --permission-mode plan

System & User Prompts

System Prompt (Output Style)

Controls how Claude responds and behaves.

Static (Markdown) (config/global/prompts/system.md):

You are a helpful coding assistant.
Write clean, maintainable code.
Follow best practices.

Dynamic (TypeScript) (config/global/prompts/system.ts):

import{createPrompt}from"@/config/helpers";exportdefaultcreatePrompt((context)=>`You are working in ${context.workingDirectory}${context.isGitRepo() ? `Current branch: ${context.getGitBranch()}` : ""}Write clean, maintainable code.`,);

Append Mode (adds to previous layers):

import{createAppendPrompt}from"@/config/helpers";exportdefaultcreateAppendPrompt((context)=>`Additional instructions for this preset.`,);

You can also use Markdown files in append mode, just name them: <target>.append.md

User Prompt (CLAUDE.md)

Project-specific instructions and context. See config/global/prompts/user.ts for a full example:

import{createPrompt}from"@/config/helpers";exportdefaultcreatePrompt((context)=>`# CRITICAL RULESDo exactly what the user asks. No alternatives, no "better" solutions...Working in: ${context.workingDirectory}Git branch: ${context.getGitBranch()}`,);

Commands

Custom slash commands available in Claude. See config/global/commands/ for examples:

Static (Markdown) (config/global/commands/review.md):

# Review
Review: "$ARGUMENTS"
You are conducting a code review...

Dynamic (TypeScript):

import{createCommand}from"@/config/helpers";exportdefaultcreateCommand((context)=>`# Custom CommandWorking in ${context.workingDirectory}Current branch: ${context.getGitBranch()}Your command instructions here...`,);

Append to existing command:

import{createAppendCommand}from"@/config/helpers";exportdefaultcreateAppendCommand((context)=>`Additional instructions for TypeScript projects...`,);

Skills

Skills are reusable instruction bundles that Claude can invoke via the Skill tool. Define them as folders under skills/ with either SKILL.md (static) or SKILL.ts (structured) and any supporting files (e.g. references/*.md).

Static (Markdown) (config/global/skills/my-skill/SKILL.md):

---name: my-skilldescription: Quick checks for the current repoallowed-tools:
- Read
- Grep---
Use this skill to run quick repository checks and summarize findings.

Structured (TypeScript) (config/global/skills/my-skill/SKILL.ts):

import{createSkill}from"@/config/helpers";exportdefaultcreateSkill((context)=>({description: `Checks for ${context.project.name}`,content: `Run targeted analysis for ${context.workingDirectory}.Use $ARGUMENTS to accept parameters.`,allowedTools: ["Read","Grep"],userInvocable: true,disableModelInvocation: false,hooks: {PreToolUse: [{matcher: "Bash",hooks: [{type: "command",command: "echo 'Skill hook'"}],},],},}));

Skill layers are resolved global, then preset, then project. A structured skill may set mode: "append" to append its SKILL.md body to an earlier skill with the same name while keeping the base skill metadata; omitted mode means override.

Hooks

Event handlers that run at specific Claude events. See config/global/hooks.ts for examples:

Examples for global hooks:

importpfrom"picocolors";import{createHook}from"@/hooks/hook-generator";import{createConfigHooks}from"@/config/helpers";constbashDenyList=[{match: /^\bgit\bcheckout/,message: "You are not allowed to do checkouts or resets",},{match: /^\bgrep\b(?!.*\|)/,message: "Use 'rg' (ripgrep) instead of 'grep' for better performance",},];constsessionStartHook=createHook({event: "SessionStart",id: "global-session-start",handler: (input)=>{consttimestamp=newDate().toISOString();console.log(p.dim("🞄"));console.log(`🚀 Session started from ${p.yellow(input.source)} at ${p.blue(timestamp)}`,);console.log(`📍 Working directory: ${p.yellow(process.cwd())}`);console.log(`🔧 Node version: ${p.yellow(process.version)}`);console.log(p.dim("🞄"));},});constpreBashValidationHook=createHook({event: "PreToolUse",id: "bash-deny-list",handler: (input)=>{constcommand=input.tool_input.commandasstring;if(input.tool_name!=="Bash"||!command)return;constfirstMatchingRule=bashDenyList.find((rule)=>command.match(rule.match),);if(!firstMatchingRule)return;return{continue: true,decision: "block",reason: firstMatchingRule?.message,};},});exportdefaultcreateConfigHooks({SessionStart: [{hooks: [sessionStartHook]}],PreToolUse: [{hooks: [preBashValidationHook]}],});

Hook Options:

constoneTimeHook=createHook({event: "SessionStart",id: "one-time-setup",handler: (input)=>{// this will only run once per sessionconsole.log("First session start only");},timeout: 5,// optional: timeout in secondsonce: true,// optional: run only first time per session});

TypeScript Validation Example (config/presets/typescript/hooks.ts):

import{$}from"zx";import{createHook}from"@/hooks/hook-generator";import{createConfigHooks}from"@/config/helpers";exportdefaultcreateConfigHooks({Stop: [{hooks: [createHook({event: "Stop",id: "typescript-validation",handler: async()=>{constresult=await$`tsc --noEmit`;if(result.exitCode!==0){return{continue: true,decision: "block",reason: `Failed tsc --noEmit:\n${result.text()}`,};}return{suppressOutput: true};},}),],},],});

Agents

Specialized sub-agents for specific tasks. See config/global/agents/ for examples:

Static (Markdown) (config/global/agents/code-reviewer.md):

---name: code-reviewerdescription: Reviews code for quality and best practicestools: [Read, Grep, Glob, Bash]---# Code Reviewer Agent
You are a specialized code review agent conducting **SYSTEMATIC, EVIDENCE-FIRST CODE REVIEWS**.
## Core Principles**EVIDENCE BEFORE OPINION** - Always provide file:line references...

Dynamic (TypeScript):

import{createAgent}from"@/config/helpers";exportdefaultcreateAgent((context)=>`---name: debuggerdescription: Debug issues in ${context.project.name}tools: [Read, Edit, Bash, Grep, Glob]---# Debugger AgentYou are debugging code in ${context.workingDirectory}Current branch: ${context.getGitBranch()}`,);

MCPs

Model Context Protocol servers for extending Claude's capabilities. See config/global/mcps/ for examples:

External MCPs

import{createConfigMCPs}from"@/config/helpers";exportdefaultcreateConfigMCPs({filesystem: {command: "npx",args: ["@modelcontextprotocol/server-filesystem"],env: {FS_ROOT: "/home/user"},},});

Filtering MCP Tools

You can filter which tools are exposed from an external MCP:

import{createConfigMCPs}from"@/config/helpers";exportdefaultcreateConfigMCPs({nixos: {command: "nix",args: ["run","github:utensils/mcp-nixos","--"],filter: (tool)=>{// Exclude specific toolsreturntool.name!=="nixos_search";},},});

The filter function receives a tool object with name and description properties. Return true to include the tool, false to exclude it.

Custom MCPs

You can easily define custom MCPs in your config using FastMCP.

import{FastMCP}from"fastmcp";import{z}from"zod";import{createConfigMCPs,createMCP}from"@/config/helpers";constcustomTools=createMCP((context)=>{constserver=newFastMCP({name: "custom-tools",version: "1.0.0",});server.addTool({name: "getProjectInfo",description: "Get current project information",parameters: z.object({}),execute: async()=>{returnJSON.stringify({directory: context.workingDirectory,branch: context.getGitBranch(),isGitRepo: context.isGitRepo(),},null,2,);},});returnserver;});exportdefaultcreateConfigMCPs({"custom-tools": customTools,});

Plugins

CCC configures Claude Code plugins via layered plugins.ts files (global/preset/project). Claude plugin settings live under the claude namespace.

Workflow

  1. Install plugins using the /plugin command
  2. Find plugin keys in ~/.claude/plugins/installed_plugins.json
  3. Enable plugins in your plugins.ts layer

Enabling Claude Plugins

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({claude: {enabledPlugins: {// Use keys from ~/.claude/plugins/installed_plugins.json"typescript-lsp@claude-plugins-official": true,"gopls-lsp@claude-plugins-official": true,},},});

Local Plugin Directories

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({claude: {pluginDirs: ["./claude-plugins/my-plugin",],},});

You can also drop local plugins into config/claude-plugins/ and CCC will auto-discover them. CLI override: ccc --plugin-dir ./path/to/plugin

LSP Plugin Support

LSP plugins are supported natively. Just enable your LSP plugins normally:

exportdefaultcreateConfigPlugins({claude: {enabledPlugins: {"typescript-lsp@claude-plugins-official": true,},},});

CCC Plugins

CCC has its own plugin system for bundling reusable configuration components. Unlike Claude's built-in plugins (configured via plugins.ts under claude), CCC plugins are local TypeScript modules that can define commands, agents, MCPs, hooks, and prompts dynamically, and which have access to enriched information about the current session.

CCC Plugins vs Claude Plugins

AspectCCC Plugins (plugins.tsccc)Claude Plugins (plugins.tsclaude)
Locationconfig/plugins/ directory~/.claude/plugins/ (installed) or local roots via config/claude-plugins/
FormatTypeScript with createPlugin()Claude's plugin format
Componentscommands, agents, MCPs, hooks, promptsDefined by Claude
DistributionLocal to your configVia /plugin command

Plugin Structure

A CCC plugin requires two files:

config/plugins/my-plugin/
├── plugin.json # Manifest with name, version, description
└── index.ts # Plugin definition using createPlugin()

plugin.json (manifest):

{
"name": "my-plugin",
"version": "1.0.0",
"description": "A custom CCC plugin"
}

index.ts (definition):

import{createPlugin}from"@/config/helpers";exportdefaultcreatePlugin({// Commands - custom slash commandscommands: (context)=>({"my-command": {content: `# My CommandYour command instructions here... `.trim(),mode: "override",},}),// Agents - specialized sub-agentsagents: (context)=>({"my-agent": {content: `---name: my-agentdescription: A specialized agenttools: [Read, Grep, Glob]---Agent instructions... `.trim(),mode: "override",},}),// MCPs - inline MCP serversmcps: (context)=>({"my-mcp": {type: "inline",config: ()=>createMyMCP(context),},}),// Hooks - event handlershooks: (context)=>({Stop: [{hooks: [createHook({event: "Stop",id: "plugin-stop-hook",handler: ()=>{// Hook logic},}),],},],}),// Prompts - system/user prompt additionsprompts: (context)=>({user: {content: "Additional user prompt content",mode: "append",},}),});

Enabling CCC Plugins

Enable CCC plugins via plugins.ts:

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"my-plugin": true,// Enable plugin"another-plugin": false,// Explicitly disable},});

You can also enable plugins in presets:

// config/presets/typescript/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"typescript-helpers": true,},});

Plugin Context

Plugins receive a PluginContext with access to:

{// Standard context properties
workingDirectory: string;
launcherDirectory: string;isGitRepo(): boolean;getGitBranch(): string;// ... all Context methods// Plugin-specific
manifest: PluginManifest;// Plugin's plugin.json
root: string;// Plugin directory path
settings: Record<string,unknown>;// Plugin settings// State management
state: {get<T>(key: string): T|undefined;set(key: string,value: unknown): void;clear(): void;getAll(): Record<string,unknown>;};}

Plugin State

Plugins can persist state using the built-in state API. State is automatically saved to disk and restored between sessions.

State API:

context.state.get<T>(key: string): T|undefined// Get a valuecontext.state.set(key: string,value: unknown): void// Set a valuecontext.state.clear(): void// Clear all statecontext.state.getAll(): Record<string,unknown>// Get all state

State Locations:

By default, plugin state is stored in /tmp/ccc-plugin-{name}-{sessionId}.json. The location is isolated by session ID (CCC_INSTANCE_ID) to prevent conflicts between concurrent CCC instances.

LocationPathUse Case
temp (default)/tmp/ccc-plugin-{name}-{sessionId}.jsonSession-scoped data
project{cwd}/.ccc/state/plugins/{name}.jsonProject-specific persistent data
user~/.ccc/state/plugins/{name}.jsonGlobal user preferences

Example - Stateful MCP:

exportdefaultcreatePlugin({mcps: (context)=>({"stateful-mcp": {type: "inline",config: ()=>{constserver=newFastMCP({name: "stateful",version: "1.0.0"});server.addTool({name: "save_data",parameters: z.object({key: z.string(),value: z.string()}),execute: async(args)=>{context.state.set(args.key,args.value);return"Saved!";},});server.addTool({name: "load_data",parameters: z.object({key: z.string()}),execute: async(args)=>{returncontext.state.get(args.key)??"Not found";},});returnserver;},},}),});

Plugin Settings

Plugins define settings using Zod schemas in index.ts. The type is automatically inferred:

// my-plugin/index.tsimport{z}from"zod";import{createPlugin}from"@/config/helpers";constsettingsSchema=z.object({maxItems: z.number().default(100),mode: z.enum(["fast","balanced","thorough"]).default("balanced"),});exportdefaultcreatePlugin({
settingsSchema,mcps: (context)=>{// context.plugin.settings is typed as { maxItems: number, mode: "fast" | "balanced" | "thorough" }const{ maxItems, mode }=context.plugin.settings;// ...},});

Pass settings when enabling the plugin:

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"my-plugin": {enabled: true,settings: {maxItems: 50,mode: "fast",},},},});

Settings are validated with Zod at plugin load time. Invalid settings throw errors.

Plugin State

Plugins get a context.state API for key-value storage:

context.state.get<T>(key)// get valuecontext.state.set(key,value)// set valuecontext.state.clear()// clear all statecontext.state.getAll()// get all state

By default, state is in-memory only (not persisted). To persist state, set stateType:

exportdefaultcreatePlugin({stateType: "temp",// /tmp/ccc-plugin-{name}-{sessionId}.json (per-instance)// or:stateType: "project",// {projectRoot}/.ccc/state/plugins/{name}.json// or:stateType: "user",// ~/.ccc/state/plugins/{name}.jsonmcps: (context)=>{/* ... */},});
stateTypePathUse case
"none" (default)(in-memory only)No persistence needed
"temp"/tmp/ccc-plugin-{name}-{sessionId}.jsonPer-instance state, cleared on reboot
"project"{projectRoot}/.ccc/state/plugins/{name}.jsonProject-specific persistent state
"user"~/.ccc/state/plugins/{name}.jsonUser-wide persistent state

onLoad Hook

Plugins can define an onLoad callback for initialization:

exportdefaultcreatePlugin({onLoad: async(context)=>{console.log(`Plugin ${context.plugin.name} loaded`);},commands: ()=>({/* ... */}),});

Inter-Plugin Communication

Plugins can access other loaded plugins using getPlugin():

exportdefaultcreatePlugin({mcps: (context)=>({"my-mcp": {type: "inline",config: ()=>{constserver=newFastMCP({name: "my-mcp",version: "1.0.0"});server.addTool({name: "get_other_plugin_data",parameters: z.object({}),execute: async()=>{// Access another plugin's contextconstotherPlugin=context.getPlugin("other-plugin");if(!otherPlugin)return"Other plugin not loaded";// Read its stateconstdata=otherPlugin.state.get("shared-data");returnJSON.stringify(data);},});returnserver;},},}),});

Plugin Dependencies

Plugins can declare dependencies on other plugins in plugin.json:

{
"name": "my-plugin",
"version": "1.0.0",
"description": "Depends on base-plugin",
"dependencies": ["base-plugin", "utility-plugin"]
}

Dependencies are loaded first, ensuring they're available when your plugin loads.

Full plugin.json Schema

{
"name": "my-plugin",
"version": "1.0.0",
"description": "A complete example plugin",
"author": "Your Name",
"license": "MIT",
"homepage": "https://example.com/my-plugin",
"repository": "https://github.com/user/my-plugin",
"dependencies": ["other-plugin"]
}

Note: Settings are defined via Zod schema in index.ts, not in plugin.json.

Example: Complete Plugin

Here's a complete example of a plugin with commands, an MCP, and hooks:

// config/plugins/task-tracker/index.tsimport{FastMCP}from"fastmcp";import{z}from"zod";import{createPlugin}from"@/config/helpers";import{createHook}from"@/hooks/hook-generator";exportdefaultcreatePlugin({commands: ()=>({"track": {content: `# Task TrackerTrack a new task: "$ARGUMENTS"Use the task_add MCP tool to add this task. `.trim(),mode: "override",},}),mcps: (context)=>({"task-tracker": {type: "inline",config: ()=>{constserver=newFastMCP({name: "task-tracker",version: "1.0.0",});server.addTool({name: "task_add",description: "Add a new task",parameters: z.object({title: z.string(),priority: z.enum(["low","medium","high"]).default("medium"),}),execute: async(args)=>{consttasks=context.state.get<string[]>("tasks")??[];tasks.push(`[${args.priority}] ${args.title}`);context.state.set("tasks",tasks);return`Added: ${args.title}`;},});server.addTool({name: "task_list",description: "List all tasks",parameters: z.object({}),execute: async()=>{consttasks=context.state.get<string[]>("tasks")??[];returntasks.length>0 ? tasks.join("\n") : "No tasks";},});returnserver;},},}),hooks: ()=>({SessionStart: [{hooks: [createHook({event: "SessionStart",id: "task-tracker-init",handler: ()=>{console.log("📋 Task Tracker plugin loaded");},}),],},],}),});

Viewing Plugin Info

Use ccc --print-config to see loaded CCC plugins:

CCC Plugins:
my-plugin (v1.0.0) [enabled]
Commands: my-plugin:my-command
MCPs: my-plugin:my-mcp
Hooks: Stop(1)

Note: Plugin components are namespaced with the plugin name (e.g., my-plugin:my-command).

Runtime Patches

All CLI patches are applied at runtime - the original node_modules files are never modified. The launcher reads the CLI, applies patches, writes to a temp file, and imports that instead.

Built-in Patches

Applied automatically on every launch:

  • Disable pr-comments and security-review features

User-defined Patches

Add custom string replacements via settings:

exportdefaultcreateConfigSettings({patches: [{find: "ultrathink",replace: "uuu"},// shorter alias],});

Patches are applied after built-in patches. No reinstall needed when changing configuration.

Statusline

Customize the Claude statusline with a simple configuration-based approach.

Priority Order

  1. config/global/statusline.ts - If this file exists, it will be executed with bun
  2. settings.statusLine - Otherwise, use the statusLine configuration from settings
  3. None - If neither is configured, no statusline is displayed

Creating a Statusline

Create config/global/statusline.ts:

import{createStatusline}from"@/config/helpers";importtype{StatusLineInput}from"@/types/statusline";exportdefaultcreateStatusline(async(data: StatusLineInput)=>{constmodelIcon=data.model?.id?.includes("opus") ? "🦆" : "🐇";constcomponents=[];// Model and iconcomponents.push(`${modelIcon}${data.model.display_name}`);// Working directoryif(data.workspace){constdir=data.workspace.project_dir||data.workspace.current_dir;constshortDir=dir.split("/").slice(-2).join("/");components.push(`📁 ${shortDir}`);}// Hook event (if present)if(data.hook_event_name){components.push(`⚡ ${data.hook_event_name}`);}console.log(components.join(" │ "));});

Using External Tools

You can also integrate external statusline tools:

import{createStatusline}from"@/config/helpers";import{$}from"bun";exportdefaultcreateStatusline(async(data)=>{// Use external ccstatusline toolconstoutput=await$`echo ${JSON.stringify(data)} | bunx ccstatusline`.text();constmodelIcon=data.model?.id?.includes("opus") ? "🦆" : "🐇";console.log(`${modelIcon}${output.trim()}`);});

StatusLineInput Type

The statusline function receives a StatusLineInput object with:

  • model.id - Model identifier (e.g., "claude-3-opus-20240229")
  • model.display_name - Human-readable model name
  • workspace.current_dir - Current working directory
  • workspace.project_dir - Project root directory
  • hook_event_name - Current hook event being executed
  • session_id - Current session identifier
  • transcript_path - Path to the transcript file
  • cwd - Current working directory
  • output_style - Output style configuration

Settings Configuration

Alternatively, configure a custom statusline command in settings.ts:

exportdefaultcreateConfigSettings({statusLine: {type: "command",command: "/path/to/your/statusline-script",},});

Note: Unlike other configuration types, statuslines do NOT support layering or merging. Only the global configuration or settings are used.

Doctor (Config Inspector)

Use ccc --doctor to print a diagnostic report of your merged configuration without launching Claude:

ccc --doctor
ccc --doctor --json

The report shows:

  • Presets detected and project configuration in use
  • Layering traces (override/append) for system/user prompts
  • Per-command and per-agent layering traces across global/presets/project
  • MCP servers and their transport type

Dump Configuration

Use ccc --dump-config to create a complete dump of the computed configuration that Claude sees:

ccc --dump-config

This creates a .config-dump/{timestamp}/ directory containing:

  • system.md - The actual computed system prompt
  • user.md - The actual computed user prompt
  • commands/ - All command files as Claude sees them
  • agents/ - All agent files as Claude sees them
  • settings.json - The merged settings
  • mcps.json - The computed MCP configurations
  • metadata.json - Context and dump information

This is useful for debugging configuration issues and understanding exactly what Claude sees.

Debug MCPs

Use ccc --debug-mcp <mcp-name> to launch the MCP Inspector for debugging MCP servers:

ccc --debug-mcp filesystem
ccc --debug-mcp custom-tools

This launches the MCP Inspector with your MCP server, allowing you to:

  • View all available tools, resources, and prompts
  • Test tool invocations interactively
  • Inspect request/response payloads
  • Debug filtered MCPs (shows tools after filtering)

Note:

  • Only works with stdio transport MCPs (not HTTP/SSE)
  • Filtered MCPs will show the filtered tools, not the original ones
  • Inline MCPs (created with FastMCP) are supported

Project Configuration

Create a project-specific configuration:

// config/projects/myapp/project.tsexportdefault{name: "myapp",root: "/path/to/myapp",disableParentClaudeMds: false,// optional, will disable Claude's behavior of loading upper CLAUDE.md files};
// config/projects/myapp/settings.tsimport{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({env: {NODE_ENV: "development",API_URL: "http://localhost:3000",},});

Context Object

All dynamic configurations receive a context object with a few utilities:

{
workingDirectory: string;// Current working directory
launcherDirectory: string;// Path to launcher installation
instanceId: string;// Unique instance identifier
project: Project;// Project instance with config
mcpServers?: Record<string,ClaudeMCPConfig>;// Processed MCP configs for this runisGitRepo(): boolean;// Check if in git repositorygetGitBranch(): string;// Current git branchgetGitStatus(): string;// Git status (porcelain)getGitRecentCommits(n): string;// Recent commit historygetDirectoryTree(): string;// Directory structuregetPlatform(): string;// OS platformgetOsVersion(): string;// OS version infogetCurrentDateTime(): string;// ISO timestamphasMCP(name: string): boolean;// True if MCP with name is configured}

Other things

  • ?

License

MIT License. See LICENSE for details.

About

Custom launcher for Claude Code, supporting dynamic prompts, layered configuration and easy custom hooks and MCPs.

Topics

Resources

Stars

19 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

image

CCC is a launcher for Claude Code that lets you configure prompts, commands, agents, hooks, and MCPs from a single place, in a layered way.

What you get

  • Dynamic Configuration: Generate system/user prompts, commands, agents dynamically.
  • Layered config: Merge global configuration with presets and project overrides.
  • Low‑effort extensibility: write hooks & MCPs in TypeScript with tiny helpers.
image

Not affiliated with Anthropic. Uses the official @anthropic-ai/claude-code CLI.

Warning: Not tested on Windows, open an issue if you run into problems.


Getting Started

1. Setup

# clone this repo somewhere you'd like to keep going to edit your configuration
git clone https://github.com/3rd/ccc.git ~/my-claude-launcher
cd~/my-claude-launcher
# install dependencies and link `ccc`
bun install
bun link
# install tsx globally (required for runtime interception)
bun add -g tsx

Note: Claude Code (@anthropic-ai/claude-code) is included as a dependency.
Bun must also be available on PATH at runtime because CCC launches hook and statusline helpers with bun.
To update it to the latest version do a bun update.

2. Customize your config

Your configuration lives in the ./config directory, which includes some examples by default.

~/my-claude-launcher/ # Your copy of this repository
└── config/
├── global/ # Global configuration
│ ├── prompts/ # System (output style) / user (CLAUDE.md) prompts
│ ├── commands/ # Your commands
│ ├── agents/ # Your sub-agents
│ ├── skills/ # Your skills
│ ├── hooks.ts # Your hooks
│ └── mcps.ts # Your MCPs
├── presets/ # Your language/framework/whatever-specific configs
│ └── typescript/ # Example: TypeScript-specific settings
└── projects/ # Your project-specific overrides
└── myapp/ # Example: Settings for your 'myapp' project

Development Mode: If a ./dev-config directory exists, it will be used instead of ./config. This allows you to keep the example configuration in ./config (committed to git) while using ./dev-config for your actual development configuration.

3. Use it

Your workflow:

  1. Edit your config in ~/my-claude-launcher/config/
  2. Run ccc instead of claude from anywhere
  3. Your config is dynamically built and loaded
ccc # wrap and launch claude
ccc --continue # all the arguments you pass will be passed through to claude# except these special cases used for debugging
ccc --doctor
ccc --print-config
ccc --print-system-prompt
ccc --print-user-prompt
ccc --dump-config
ccc --debug-mcp <mcp-name>
ccc --doru # launcher-only flag; place it before Claude args
ccc --doru --continue

ccc --doru runs CCC through npx doru --ui and auto-opens doru's live UI. Treat --doru as a launcher-only leading flag and place it before any Claude args. The first run may download doru, and doru itself requires npx plus Node.js 22+.

Configuration Layers

ccc loads configurations in layers (later overrides earlier):

  1. Globalconfig/global/ - Base configuration for all projects
  2. Presetsconfig/presets/ - Auto-detected based on project type
  3. Projectsconfig/projects/ - Specific project overrides

Each layer can define:

  • settings.ts - Settings that will go into Claude Code's settings.json
  • prompts/user.{md,ts} - User instructions (CLAUDE.md)
  • prompts/system.{md,ts} - Output style
  • commands/*.{md,ts} - Custom slash commands
  • agents/*.{md,ts} - Custom sub-agents
  • skills/*/SKILL.{md,ts} - Custom skills (and supporting files)
  • hooks.ts - Custom hooks
  • mcps.ts - Custom MCPs

How It Works

ccc injects configurations using a virtual filesystem overlay. Your actual Claude installation remains untouched. Configurations are injected at runtime through Node.js module interception.

The launcher:

  1. Discovers and merges configurations from all layers
  2. Generates a vfs with the merged config
  3. Intercepts Node's modules to serve virtual files
  4. Launches Claude with the injected configuration
Global ┐
Preset ├─► merge ─► "virtual overlay" ─► Claude Code
Project ┘

Using Models from Other Vendors

You can configure CCC to use models from other vendors by setting environment variables in your settings.ts. This allows you to use models like GLM, Kimi K2, or Deepseek through their Anthropic-compatible APIs.

Configuration Examples

Add these environment variables to your config/global/settings.ts:

import{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({env: {// GLM 4.5ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",ANTHROPIC_AUTH_TOKEN: "Z_API_KEY",ANTHROPIC_MODEL: "glm-4.5",ANTHROPIC_FAST_MODEL: "glm-4.5-air",// Kimi K2// ANTHROPIC_BASE_URL: "https://api.moonshot.ai/anthropic",// ANTHROPIC_AUTH_TOKEN: "KIMI_API_KEY",// Deepseek// ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",// ANTHROPIC_AUTH_TOKEN: "DEEPSEEK_API_KEY",// ANTHROPIC_MODEL: "deepseek-chat",// ANTHROPIC_FAST_MODEL: "deepseek-chat"}});

Environment Variables

  • ANTHROPIC_BASE_URL - The base URL for the vendor's API endpoint
  • ANTHROPIC_AUTH_TOKEN - Your API key for the vendor
  • ANTHROPIC_MODEL - The main model to use (e.g., "glm-4.5", "deepseek-chat")
  • ANTHROPIC_FAST_MODEL - The model to use for quick operations (optional)

Usage

Once configured, CCC will automatically use the specified vendor's models instead of Anthropic's models. All CCC features like prompts, commands, agents, hooks, and MCPs will continue to work with the alternative models.

Note: Make sure you have the required API keys and that the vendor's API is compatible with Anthropic's API format.

Extra Configuration

Some settings will still be read from your global ~/.claude.json:

# things like these:
claude config set -g autocheckpointingEnabled true
claude config set -g diffTool delta
claude config set -g supervisorMode true
claude config set -g autoCompactEnabled true
claude config set --global preferredNotifChannel terminal_bell
claude config set -g verbose true

CLI argument settings

Some settings are passed directly as CLI arguments to Claude rather than being written to settings.json. These are grouped under settings.cli. See Claude Code CLI Reference for details.

// config/global/settings.tsimport{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({cli: {tools: ["Bash","Read","Edit","Write"],disallowedTools: ["WebSearch","WebFetch"],allowedTools: ["Read","Glob","Grep"],addDir: ["/path/to/shared/libs"],permissionMode: "plan",verbose: true,debug: "api,hooks",chrome: true,ide: true,enableLspLogging: false,agent: "code-reviewer",},// other settings go to settings.json as normalenv: { ... },});

Available CLI-Only Settings

SettingTypeCLI FlagDescription
toolsstring[] | "default"--tools "Tool1,Tool2"Available tools ("default" for all, [] to disable)
disallowedToolsstring[]--disallowedTools "Tool1,Tool2"Tools removed from model context entirely
allowedToolsstring[]--allowedTools "Tool1,Tool2"Tools that execute without permission prompts
addDirstring[]--add-dir pathAdditional directories Claude can access
permissionModeenum--permission-mode modeStart mode: default, acceptEdits, plan, bypassPermissions
verboseboolean--verboseEnable verbose logging
debugboolean | string--debug [filter]Enable debug mode, optionally with category filter
chromeboolean--chrome / --no-chromeEnable/disable Chrome browser integration
ideboolean--ideAuto-connect to IDE on startup
enableLspLoggingboolean--enable-lsp-loggingEnable verbose LSP logging
agentstring--agent nameDefault agent for the session
agentsRecord<string, AgentDef>--agents JSONCustom subagent definitions
forkSessionboolean--fork-sessionCreate new session ID when resuming
fallbackModelstring--fallback-model nameFallback model when primary is overloaded
settingSourcesstring[]--setting-sources "user,project,local"Config sources to use
strictMcpConfigboolean--strict-mcp-configOnly use specified MCP config

CLI Override

CLI arguments provided directly to ccc override the corresponding settings.cli values:

# override disallowedTools from settings
ccc --disallowedTools "WebSearch,WebFetch"# start in plan mode regardless of settings
ccc --permission-mode plan

System & User Prompts

System Prompt (Output Style)

Controls how Claude responds and behaves.

Static (Markdown) (config/global/prompts/system.md):

You are a helpful coding assistant.
Write clean, maintainable code.
Follow best practices.

Dynamic (TypeScript) (config/global/prompts/system.ts):

import{createPrompt}from"@/config/helpers";exportdefaultcreatePrompt((context)=>`You are working in ${context.workingDirectory}${context.isGitRepo() ? `Current branch: ${context.getGitBranch()}` : ""}Write clean, maintainable code.`,);

Append Mode (adds to previous layers):

import{createAppendPrompt}from"@/config/helpers";exportdefaultcreateAppendPrompt((context)=>`Additional instructions for this preset.`,);

You can also use Markdown files in append mode, just name them: <target>.append.md

User Prompt (CLAUDE.md)

Project-specific instructions and context. See config/global/prompts/user.ts for a full example:

import{createPrompt}from"@/config/helpers";exportdefaultcreatePrompt((context)=>`# CRITICAL RULESDo exactly what the user asks. No alternatives, no "better" solutions...Working in: ${context.workingDirectory}Git branch: ${context.getGitBranch()}`,);

Commands

Custom slash commands available in Claude. See config/global/commands/ for examples:

Static (Markdown) (config/global/commands/review.md):

# Review
Review: "$ARGUMENTS"
You are conducting a code review...

Dynamic (TypeScript):

import{createCommand}from"@/config/helpers";exportdefaultcreateCommand((context)=>`# Custom CommandWorking in ${context.workingDirectory}Current branch: ${context.getGitBranch()}Your command instructions here...`,);

Append to existing command:

import{createAppendCommand}from"@/config/helpers";exportdefaultcreateAppendCommand((context)=>`Additional instructions for TypeScript projects...`,);

Skills

Skills are reusable instruction bundles that Claude can invoke via the Skill tool. Define them as folders under skills/ with either SKILL.md (static) or SKILL.ts (structured) and any supporting files (e.g. references/*.md).

Static (Markdown) (config/global/skills/my-skill/SKILL.md):

---name: my-skilldescription: Quick checks for the current repoallowed-tools:
- Read
- Grep---
Use this skill to run quick repository checks and summarize findings.

Structured (TypeScript) (config/global/skills/my-skill/SKILL.ts):

import{createSkill}from"@/config/helpers";exportdefaultcreateSkill((context)=>({description: `Checks for ${context.project.name}`,content: `Run targeted analysis for ${context.workingDirectory}.Use $ARGUMENTS to accept parameters.`,allowedTools: ["Read","Grep"],userInvocable: true,disableModelInvocation: false,hooks: {PreToolUse: [{matcher: "Bash",hooks: [{type: "command",command: "echo 'Skill hook'"}],},],},}));

Skill layers are resolved global, then preset, then project. A structured skill may set mode: "append" to append its SKILL.md body to an earlier skill with the same name while keeping the base skill metadata; omitted mode means override.

Hooks

Event handlers that run at specific Claude events. See config/global/hooks.ts for examples:

Examples for global hooks:

importpfrom"picocolors";import{createHook}from"@/hooks/hook-generator";import{createConfigHooks}from"@/config/helpers";constbashDenyList=[{match: /^\bgit\bcheckout/,message: "You are not allowed to do checkouts or resets",},{match: /^\bgrep\b(?!.*\|)/,message: "Use 'rg' (ripgrep) instead of 'grep' for better performance",},];constsessionStartHook=createHook({event: "SessionStart",id: "global-session-start",handler: (input)=>{consttimestamp=newDate().toISOString();console.log(p.dim("🞄"));console.log(`🚀 Session started from ${p.yellow(input.source)} at ${p.blue(timestamp)}`,);console.log(`📍 Working directory: ${p.yellow(process.cwd())}`);console.log(`🔧 Node version: ${p.yellow(process.version)}`);console.log(p.dim("🞄"));},});constpreBashValidationHook=createHook({event: "PreToolUse",id: "bash-deny-list",handler: (input)=>{constcommand=input.tool_input.commandasstring;if(input.tool_name!=="Bash"||!command)return;constfirstMatchingRule=bashDenyList.find((rule)=>command.match(rule.match),);if(!firstMatchingRule)return;return{continue: true,decision: "block",reason: firstMatchingRule?.message,};},});exportdefaultcreateConfigHooks({SessionStart: [{hooks: [sessionStartHook]}],PreToolUse: [{hooks: [preBashValidationHook]}],});

Hook Options:

constoneTimeHook=createHook({event: "SessionStart",id: "one-time-setup",handler: (input)=>{// this will only run once per sessionconsole.log("First session start only");},timeout: 5,// optional: timeout in secondsonce: true,// optional: run only first time per session});

TypeScript Validation Example (config/presets/typescript/hooks.ts):

import{$}from"zx";import{createHook}from"@/hooks/hook-generator";import{createConfigHooks}from"@/config/helpers";exportdefaultcreateConfigHooks({Stop: [{hooks: [createHook({event: "Stop",id: "typescript-validation",handler: async()=>{constresult=await$`tsc --noEmit`;if(result.exitCode!==0){return{continue: true,decision: "block",reason: `Failed tsc --noEmit:\n${result.text()}`,};}return{suppressOutput: true};},}),],},],});

Agents

Specialized sub-agents for specific tasks. See config/global/agents/ for examples:

Static (Markdown) (config/global/agents/code-reviewer.md):

---name: code-reviewerdescription: Reviews code for quality and best practicestools: [Read, Grep, Glob, Bash]---# Code Reviewer Agent
You are a specialized code review agent conducting **SYSTEMATIC, EVIDENCE-FIRST CODE REVIEWS**.
## Core Principles**EVIDENCE BEFORE OPINION** - Always provide file:line references...

Dynamic (TypeScript):

import{createAgent}from"@/config/helpers";exportdefaultcreateAgent((context)=>`---name: debuggerdescription: Debug issues in ${context.project.name}tools: [Read, Edit, Bash, Grep, Glob]---# Debugger AgentYou are debugging code in ${context.workingDirectory}Current branch: ${context.getGitBranch()}`,);

MCPs

Model Context Protocol servers for extending Claude's capabilities. See config/global/mcps/ for examples:

External MCPs

import{createConfigMCPs}from"@/config/helpers";exportdefaultcreateConfigMCPs({filesystem: {command: "npx",args: ["@modelcontextprotocol/server-filesystem"],env: {FS_ROOT: "/home/user"},},});

Filtering MCP Tools

You can filter which tools are exposed from an external MCP:

import{createConfigMCPs}from"@/config/helpers";exportdefaultcreateConfigMCPs({nixos: {command: "nix",args: ["run","github:utensils/mcp-nixos","--"],filter: (tool)=>{// Exclude specific toolsreturntool.name!=="nixos_search";},},});

The filter function receives a tool object with name and description properties. Return true to include the tool, false to exclude it.

Custom MCPs

You can easily define custom MCPs in your config using FastMCP.

import{FastMCP}from"fastmcp";import{z}from"zod";import{createConfigMCPs,createMCP}from"@/config/helpers";constcustomTools=createMCP((context)=>{constserver=newFastMCP({name: "custom-tools",version: "1.0.0",});server.addTool({name: "getProjectInfo",description: "Get current project information",parameters: z.object({}),execute: async()=>{returnJSON.stringify({directory: context.workingDirectory,branch: context.getGitBranch(),isGitRepo: context.isGitRepo(),},null,2,);},});returnserver;});exportdefaultcreateConfigMCPs({"custom-tools": customTools,});

Plugins

CCC configures Claude Code plugins via layered plugins.ts files (global/preset/project). Claude plugin settings live under the claude namespace.

Workflow

  1. Install plugins using the /plugin command
  2. Find plugin keys in ~/.claude/plugins/installed_plugins.json
  3. Enable plugins in your plugins.ts layer

Enabling Claude Plugins

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({claude: {enabledPlugins: {// Use keys from ~/.claude/plugins/installed_plugins.json"typescript-lsp@claude-plugins-official": true,"gopls-lsp@claude-plugins-official": true,},},});

Local Plugin Directories

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({claude: {pluginDirs: ["./claude-plugins/my-plugin",],},});

You can also drop local plugins into config/claude-plugins/ and CCC will auto-discover them. CLI override: ccc --plugin-dir ./path/to/plugin

LSP Plugin Support

LSP plugins are supported natively. Just enable your LSP plugins normally:

exportdefaultcreateConfigPlugins({claude: {enabledPlugins: {"typescript-lsp@claude-plugins-official": true,},},});

CCC Plugins

CCC has its own plugin system for bundling reusable configuration components. Unlike Claude's built-in plugins (configured via plugins.ts under claude), CCC plugins are local TypeScript modules that can define commands, agents, MCPs, hooks, and prompts dynamically, and which have access to enriched information about the current session.

CCC Plugins vs Claude Plugins

AspectCCC Plugins (plugins.tsccc)Claude Plugins (plugins.tsclaude)
Locationconfig/plugins/ directory~/.claude/plugins/ (installed) or local roots via config/claude-plugins/
FormatTypeScript with createPlugin()Claude's plugin format
Componentscommands, agents, MCPs, hooks, promptsDefined by Claude
DistributionLocal to your configVia /plugin command

Plugin Structure

A CCC plugin requires two files:

config/plugins/my-plugin/
├── plugin.json # Manifest with name, version, description
└── index.ts # Plugin definition using createPlugin()

plugin.json (manifest):

{
"name": "my-plugin",
"version": "1.0.0",
"description": "A custom CCC plugin"
}

index.ts (definition):

import{createPlugin}from"@/config/helpers";exportdefaultcreatePlugin({// Commands - custom slash commandscommands: (context)=>({"my-command": {content: `# My CommandYour command instructions here... `.trim(),mode: "override",},}),// Agents - specialized sub-agentsagents: (context)=>({"my-agent": {content: `---name: my-agentdescription: A specialized agenttools: [Read, Grep, Glob]---Agent instructions... `.trim(),mode: "override",},}),// MCPs - inline MCP serversmcps: (context)=>({"my-mcp": {type: "inline",config: ()=>createMyMCP(context),},}),// Hooks - event handlershooks: (context)=>({Stop: [{hooks: [createHook({event: "Stop",id: "plugin-stop-hook",handler: ()=>{// Hook logic},}),],},],}),// Prompts - system/user prompt additionsprompts: (context)=>({user: {content: "Additional user prompt content",mode: "append",},}),});

Enabling CCC Plugins

Enable CCC plugins via plugins.ts:

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"my-plugin": true,// Enable plugin"another-plugin": false,// Explicitly disable},});

You can also enable plugins in presets:

// config/presets/typescript/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"typescript-helpers": true,},});

Plugin Context

Plugins receive a PluginContext with access to:

{// Standard context properties
workingDirectory: string;
launcherDirectory: string;isGitRepo(): boolean;getGitBranch(): string;// ... all Context methods// Plugin-specific
manifest: PluginManifest;// Plugin's plugin.json
root: string;// Plugin directory path
settings: Record<string,unknown>;// Plugin settings// State management
state: {get<T>(key: string): T|undefined;set(key: string,value: unknown): void;clear(): void;getAll(): Record<string,unknown>;};}

Plugin State

Plugins can persist state using the built-in state API. State is automatically saved to disk and restored between sessions.

State API:

context.state.get<T>(key: string): T|undefined// Get a valuecontext.state.set(key: string,value: unknown): void// Set a valuecontext.state.clear(): void// Clear all statecontext.state.getAll(): Record<string,unknown>// Get all state

State Locations:

By default, plugin state is stored in /tmp/ccc-plugin-{name}-{sessionId}.json. The location is isolated by session ID (CCC_INSTANCE_ID) to prevent conflicts between concurrent CCC instances.

LocationPathUse Case
temp (default)/tmp/ccc-plugin-{name}-{sessionId}.jsonSession-scoped data
project{cwd}/.ccc/state/plugins/{name}.jsonProject-specific persistent data
user~/.ccc/state/plugins/{name}.jsonGlobal user preferences

Example - Stateful MCP:

exportdefaultcreatePlugin({mcps: (context)=>({"stateful-mcp": {type: "inline",config: ()=>{constserver=newFastMCP({name: "stateful",version: "1.0.0"});server.addTool({name: "save_data",parameters: z.object({key: z.string(),value: z.string()}),execute: async(args)=>{context.state.set(args.key,args.value);return"Saved!";},});server.addTool({name: "load_data",parameters: z.object({key: z.string()}),execute: async(args)=>{returncontext.state.get(args.key)??"Not found";},});returnserver;},},}),});

Plugin Settings

Plugins define settings using Zod schemas in index.ts. The type is automatically inferred:

// my-plugin/index.tsimport{z}from"zod";import{createPlugin}from"@/config/helpers";constsettingsSchema=z.object({maxItems: z.number().default(100),mode: z.enum(["fast","balanced","thorough"]).default("balanced"),});exportdefaultcreatePlugin({
settingsSchema,mcps: (context)=>{// context.plugin.settings is typed as { maxItems: number, mode: "fast" | "balanced" | "thorough" }const{ maxItems, mode }=context.plugin.settings;// ...},});

Pass settings when enabling the plugin:

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"my-plugin": {enabled: true,settings: {maxItems: 50,mode: "fast",},},},});

Settings are validated with Zod at plugin load time. Invalid settings throw errors.

Plugin State

Plugins get a context.state API for key-value storage:

context.state.get<T>(key)// get valuecontext.state.set(key,value)// set valuecontext.state.clear()// clear all statecontext.state.getAll()// get all state

By default, state is in-memory only (not persisted). To persist state, set stateType:

exportdefaultcreatePlugin({stateType: "temp",// /tmp/ccc-plugin-{name}-{sessionId}.json (per-instance)// or:stateType: "project",// {projectRoot}/.ccc/state/plugins/{name}.json// or:stateType: "user",// ~/.ccc/state/plugins/{name}.jsonmcps: (context)=>{/* ... */},});
stateTypePathUse case
"none" (default)(in-memory only)No persistence needed
"temp"/tmp/ccc-plugin-{name}-{sessionId}.jsonPer-instance state, cleared on reboot
"project"{projectRoot}/.ccc/state/plugins/{name}.jsonProject-specific persistent state
"user"~/.ccc/state/plugins/{name}.jsonUser-wide persistent state

onLoad Hook

Plugins can define an onLoad callback for initialization:

exportdefaultcreatePlugin({onLoad: async(context)=>{console.log(`Plugin ${context.plugin.name} loaded`);},commands: ()=>({/* ... */}),});

Inter-Plugin Communication

Plugins can access other loaded plugins using getPlugin():

exportdefaultcreatePlugin({mcps: (context)=>({"my-mcp": {type: "inline",config: ()=>{constserver=newFastMCP({name: "my-mcp",version: "1.0.0"});server.addTool({name: "get_other_plugin_data",parameters: z.object({}),execute: async()=>{// Access another plugin's contextconstotherPlugin=context.getPlugin("other-plugin");if(!otherPlugin)return"Other plugin not loaded";// Read its stateconstdata=otherPlugin.state.get("shared-data");returnJSON.stringify(data);},});returnserver;},},}),});

Plugin Dependencies

Plugins can declare dependencies on other plugins in plugin.json:

{
"name": "my-plugin",
"version": "1.0.0",
"description": "Depends on base-plugin",
"dependencies": ["base-plugin", "utility-plugin"]
}

Dependencies are loaded first, ensuring they're available when your plugin loads.

Full plugin.json Schema

{
"name": "my-plugin",
"version": "1.0.0",
"description": "A complete example plugin",
"author": "Your Name",
"license": "MIT",
"homepage": "https://example.com/my-plugin",
"repository": "https://github.com/user/my-plugin",
"dependencies": ["other-plugin"]
}

Note: Settings are defined via Zod schema in index.ts, not in plugin.json.

Example: Complete Plugin

Here's a complete example of a plugin with commands, an MCP, and hooks:

// config/plugins/task-tracker/index.tsimport{FastMCP}from"fastmcp";import{z}from"zod";import{createPlugin}from"@/config/helpers";import{createHook}from"@/hooks/hook-generator";exportdefaultcreatePlugin({commands: ()=>({"track": {content: `# Task TrackerTrack a new task: "$ARGUMENTS"Use the task_add MCP tool to add this task. `.trim(),mode: "override",},}),mcps: (context)=>({"task-tracker": {type: "inline",config: ()=>{constserver=newFastMCP({name: "task-tracker",version: "1.0.0",});server.addTool({name: "task_add",description: "Add a new task",parameters: z.object({title: z.string(),priority: z.enum(["low","medium","high"]).default("medium"),}),execute: async(args)=>{consttasks=context.state.get<string[]>("tasks")??[];tasks.push(`[${args.priority}] ${args.title}`);context.state.set("tasks",tasks);return`Added: ${args.title}`;},});server.addTool({name: "task_list",description: "List all tasks",parameters: z.object({}),execute: async()=>{consttasks=context.state.get<string[]>("tasks")??[];returntasks.length>0 ? tasks.join("\n") : "No tasks";},});returnserver;},},}),hooks: ()=>({SessionStart: [{hooks: [createHook({event: "SessionStart",id: "task-tracker-init",handler: ()=>{console.log("📋 Task Tracker plugin loaded");},}),],},],}),});

Viewing Plugin Info

Use ccc --print-config to see loaded CCC plugins:

CCC Plugins:
my-plugin (v1.0.0) [enabled]
Commands: my-plugin:my-command
MCPs: my-plugin:my-mcp
Hooks: Stop(1)

Note: Plugin components are namespaced with the plugin name (e.g., my-plugin:my-command).

Runtime Patches

All CLI patches are applied at runtime - the original node_modules files are never modified. The launcher reads the CLI, applies patches, writes to a temp file, and imports that instead.

Built-in Patches

Applied automatically on every launch:

  • Disable pr-comments and security-review features

User-defined Patches

Add custom string replacements via settings:

exportdefaultcreateConfigSettings({patches: [{find: "ultrathink",replace: "uuu"},// shorter alias],});

Patches are applied after built-in patches. No reinstall needed when changing configuration.

Statusline

Customize the Claude statusline with a simple configuration-based approach.

Priority Order

  1. config/global/statusline.ts - If this file exists, it will be executed with bun
  2. settings.statusLine - Otherwise, use the statusLine configuration from settings
  3. None - If neither is configured, no statusline is displayed

Creating a Statusline

Create config/global/statusline.ts:

import{createStatusline}from"@/config/helpers";importtype{StatusLineInput}from"@/types/statusline";exportdefaultcreateStatusline(async(data: StatusLineInput)=>{constmodelIcon=data.model?.id?.includes("opus") ? "🦆" : "🐇";constcomponents=[];// Model and iconcomponents.push(`${modelIcon}${data.model.display_name}`);// Working directoryif(data.workspace){constdir=data.workspace.project_dir||data.workspace.current_dir;constshortDir=dir.split("/").slice(-2).join("/");components.push(`📁 ${shortDir}`);}// Hook event (if present)if(data.hook_event_name){components.push(`⚡ ${data.hook_event_name}`);}console.log(components.join(" │ "));});

Using External Tools

You can also integrate external statusline tools:

import{createStatusline}from"@/config/helpers";import{$}from"bun";exportdefaultcreateStatusline(async(data)=>{// Use external ccstatusline toolconstoutput=await$`echo ${JSON.stringify(data)} | bunx ccstatusline`.text();constmodelIcon=data.model?.id?.includes("opus") ? "🦆" : "🐇";console.log(`${modelIcon}${output.trim()}`);});

StatusLineInput Type

The statusline function receives a StatusLineInput object with:

  • model.id - Model identifier (e.g., "claude-3-opus-20240229")
  • model.display_name - Human-readable model name
  • workspace.current_dir - Current working directory
  • workspace.project_dir - Project root directory
  • hook_event_name - Current hook event being executed
  • session_id - Current session identifier
  • transcript_path - Path to the transcript file
  • cwd - Current working directory
  • output_style - Output style configuration

Settings Configuration

Alternatively, configure a custom statusline command in settings.ts:

exportdefaultcreateConfigSettings({statusLine: {type: "command",command: "/path/to/your/statusline-script",},});

Note: Unlike other configuration types, statuslines do NOT support layering or merging. Only the global configuration or settings are used.

Doctor (Config Inspector)

Use ccc --doctor to print a diagnostic report of your merged configuration without launching Claude:

ccc --doctor
ccc --doctor --json

The report shows:

  • Presets detected and project configuration in use
  • Layering traces (override/append) for system/user prompts
  • Per-command and per-agent layering traces across global/presets/project
  • MCP servers and their transport type

Dump Configuration

Use ccc --dump-config to create a complete dump of the computed configuration that Claude sees:

ccc --dump-config

This creates a .config-dump/{timestamp}/ directory containing:

  • system.md - The actual computed system prompt
  • user.md - The actual computed user prompt
  • commands/ - All command files as Claude sees them
  • agents/ - All agent files as Claude sees them
  • settings.json - The merged settings
  • mcps.json - The computed MCP configurations
  • metadata.json - Context and dump information

This is useful for debugging configuration issues and understanding exactly what Claude sees.

Debug MCPs

Use ccc --debug-mcp <mcp-name> to launch the MCP Inspector for debugging MCP servers:

ccc --debug-mcp filesystem
ccc --debug-mcp custom-tools

This launches the MCP Inspector with your MCP server, allowing you to:

  • View all available tools, resources, and prompts
  • Test tool invocations interactively
  • Inspect request/response payloads
  • Debug filtered MCPs (shows tools after filtering)

Note:

  • Only works with stdio transport MCPs (not HTTP/SSE)
  • Filtered MCPs will show the filtered tools, not the original ones
  • Inline MCPs (created with FastMCP) are supported

Project Configuration

Create a project-specific configuration:

// config/projects/myapp/project.tsexportdefault{name: "myapp",root: "/path/to/myapp",disableParentClaudeMds: false,// optional, will disable Claude's behavior of loading upper CLAUDE.md files};
// config/projects/myapp/settings.tsimport{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({env: {NODE_ENV: "development",API_URL: "http://localhost:3000",},});

Context Object

All dynamic configurations receive a context object with a few utilities:

{
workingDirectory: string;// Current working directory
launcherDirectory: string;// Path to launcher installation
instanceId: string;// Unique instance identifier
project: Project;// Project instance with config
mcpServers?: Record<string,ClaudeMCPConfig>;// Processed MCP configs for this runisGitRepo(): boolean;// Check if in git repositorygetGitBranch(): string;// Current git branchgetGitStatus(): string;// Git status (porcelain)getGitRecentCommits(n): string;// Recent commit historygetDirectoryTree(): string;// Directory structuregetPlatform(): string;// OS platformgetOsVersion(): string;// OS version infogetCurrentDateTime(): string;// ISO timestamphasMCP(name: string): boolean;// True if MCP with name is configured}

Other things

  • ?

License

MIT License. See LICENSE for details.

About

Custom launcher for Claude Code, supporting dynamic prompts, layered configuration and easy custom hooks and MCPs.

Topics

Resources

Stars

19 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

image

CCC is a launcher for Claude Code that lets you configure prompts, commands, agents, hooks, and MCPs from a single place, in a layered way.

What you get

  • Dynamic Configuration: Generate system/user prompts, commands, agents dynamically.
  • Layered config: Merge global configuration with presets and project overrides.
  • Low‑effort extensibility: write hooks & MCPs in TypeScript with tiny helpers.
image

Not affiliated with Anthropic. Uses the official @anthropic-ai/claude-code CLI.

Warning: Not tested on Windows, open an issue if you run into problems.


Getting Started

1. Setup

# clone this repo somewhere you'd like to keep going to edit your configuration
git clone https://github.com/3rd/ccc.git ~/my-claude-launcher
cd~/my-claude-launcher
# install dependencies and link `ccc`
bun install
bun link
# install tsx globally (required for runtime interception)
bun add -g tsx

Note: Claude Code (@anthropic-ai/claude-code) is included as a dependency.
Bun must also be available on PATH at runtime because CCC launches hook and statusline helpers with bun.
To update it to the latest version do a bun update.

2. Customize your config

Your configuration lives in the ./config directory, which includes some examples by default.

~/my-claude-launcher/ # Your copy of this repository
└── config/
├── global/ # Global configuration
│ ├── prompts/ # System (output style) / user (CLAUDE.md) prompts
│ ├── commands/ # Your commands
│ ├── agents/ # Your sub-agents
│ ├── skills/ # Your skills
│ ├── hooks.ts # Your hooks
│ └── mcps.ts # Your MCPs
├── presets/ # Your language/framework/whatever-specific configs
│ └── typescript/ # Example: TypeScript-specific settings
└── projects/ # Your project-specific overrides
└── myapp/ # Example: Settings for your 'myapp' project

Development Mode: If a ./dev-config directory exists, it will be used instead of ./config. This allows you to keep the example configuration in ./config (committed to git) while using ./dev-config for your actual development configuration.

3. Use it

Your workflow:

  1. Edit your config in ~/my-claude-launcher/config/
  2. Run ccc instead of claude from anywhere
  3. Your config is dynamically built and loaded
ccc # wrap and launch claude
ccc --continue # all the arguments you pass will be passed through to claude# except these special cases used for debugging
ccc --doctor
ccc --print-config
ccc --print-system-prompt
ccc --print-user-prompt
ccc --dump-config
ccc --debug-mcp <mcp-name>
ccc --doru # launcher-only flag; place it before Claude args
ccc --doru --continue

ccc --doru runs CCC through npx doru --ui and auto-opens doru's live UI. Treat --doru as a launcher-only leading flag and place it before any Claude args. The first run may download doru, and doru itself requires npx plus Node.js 22+.

Configuration Layers

ccc loads configurations in layers (later overrides earlier):

  1. Globalconfig/global/ - Base configuration for all projects
  2. Presetsconfig/presets/ - Auto-detected based on project type
  3. Projectsconfig/projects/ - Specific project overrides

Each layer can define:

  • settings.ts - Settings that will go into Claude Code's settings.json
  • prompts/user.{md,ts} - User instructions (CLAUDE.md)
  • prompts/system.{md,ts} - Output style
  • commands/*.{md,ts} - Custom slash commands
  • agents/*.{md,ts} - Custom sub-agents
  • skills/*/SKILL.{md,ts} - Custom skills (and supporting files)
  • hooks.ts - Custom hooks
  • mcps.ts - Custom MCPs

How It Works

ccc injects configurations using a virtual filesystem overlay. Your actual Claude installation remains untouched. Configurations are injected at runtime through Node.js module interception.

The launcher:

  1. Discovers and merges configurations from all layers
  2. Generates a vfs with the merged config
  3. Intercepts Node's modules to serve virtual files
  4. Launches Claude with the injected configuration
Global ┐
Preset ├─► merge ─► "virtual overlay" ─► Claude Code
Project ┘

Using Models from Other Vendors

You can configure CCC to use models from other vendors by setting environment variables in your settings.ts. This allows you to use models like GLM, Kimi K2, or Deepseek through their Anthropic-compatible APIs.

Configuration Examples

Add these environment variables to your config/global/settings.ts:

import{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({env: {// GLM 4.5ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",ANTHROPIC_AUTH_TOKEN: "Z_API_KEY",ANTHROPIC_MODEL: "glm-4.5",ANTHROPIC_FAST_MODEL: "glm-4.5-air",// Kimi K2// ANTHROPIC_BASE_URL: "https://api.moonshot.ai/anthropic",// ANTHROPIC_AUTH_TOKEN: "KIMI_API_KEY",// Deepseek// ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",// ANTHROPIC_AUTH_TOKEN: "DEEPSEEK_API_KEY",// ANTHROPIC_MODEL: "deepseek-chat",// ANTHROPIC_FAST_MODEL: "deepseek-chat"}});

Environment Variables

  • ANTHROPIC_BASE_URL - The base URL for the vendor's API endpoint
  • ANTHROPIC_AUTH_TOKEN - Your API key for the vendor
  • ANTHROPIC_MODEL - The main model to use (e.g., "glm-4.5", "deepseek-chat")
  • ANTHROPIC_FAST_MODEL - The model to use for quick operations (optional)

Usage

Once configured, CCC will automatically use the specified vendor's models instead of Anthropic's models. All CCC features like prompts, commands, agents, hooks, and MCPs will continue to work with the alternative models.

Note: Make sure you have the required API keys and that the vendor's API is compatible with Anthropic's API format.

Extra Configuration

Some settings will still be read from your global ~/.claude.json:

# things like these:
claude config set -g autocheckpointingEnabled true
claude config set -g diffTool delta
claude config set -g supervisorMode true
claude config set -g autoCompactEnabled true
claude config set --global preferredNotifChannel terminal_bell
claude config set -g verbose true

CLI argument settings

Some settings are passed directly as CLI arguments to Claude rather than being written to settings.json. These are grouped under settings.cli. See Claude Code CLI Reference for details.

// config/global/settings.tsimport{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({cli: {tools: ["Bash","Read","Edit","Write"],disallowedTools: ["WebSearch","WebFetch"],allowedTools: ["Read","Glob","Grep"],addDir: ["/path/to/shared/libs"],permissionMode: "plan",verbose: true,debug: "api,hooks",chrome: true,ide: true,enableLspLogging: false,agent: "code-reviewer",},// other settings go to settings.json as normalenv: { ... },});

Available CLI-Only Settings

SettingTypeCLI FlagDescription
toolsstring[] | "default"--tools "Tool1,Tool2"Available tools ("default" for all, [] to disable)
disallowedToolsstring[]--disallowedTools "Tool1,Tool2"Tools removed from model context entirely
allowedToolsstring[]--allowedTools "Tool1,Tool2"Tools that execute without permission prompts
addDirstring[]--add-dir pathAdditional directories Claude can access
permissionModeenum--permission-mode modeStart mode: default, acceptEdits, plan, bypassPermissions
verboseboolean--verboseEnable verbose logging
debugboolean | string--debug [filter]Enable debug mode, optionally with category filter
chromeboolean--chrome / --no-chromeEnable/disable Chrome browser integration
ideboolean--ideAuto-connect to IDE on startup
enableLspLoggingboolean--enable-lsp-loggingEnable verbose LSP logging
agentstring--agent nameDefault agent for the session
agentsRecord<string, AgentDef>--agents JSONCustom subagent definitions
forkSessionboolean--fork-sessionCreate new session ID when resuming
fallbackModelstring--fallback-model nameFallback model when primary is overloaded
settingSourcesstring[]--setting-sources "user,project,local"Config sources to use
strictMcpConfigboolean--strict-mcp-configOnly use specified MCP config

CLI Override

CLI arguments provided directly to ccc override the corresponding settings.cli values:

# override disallowedTools from settings
ccc --disallowedTools "WebSearch,WebFetch"# start in plan mode regardless of settings
ccc --permission-mode plan

System & User Prompts

System Prompt (Output Style)

Controls how Claude responds and behaves.

Static (Markdown) (config/global/prompts/system.md):

You are a helpful coding assistant.
Write clean, maintainable code.
Follow best practices.

Dynamic (TypeScript) (config/global/prompts/system.ts):

import{createPrompt}from"@/config/helpers";exportdefaultcreatePrompt((context)=>`You are working in ${context.workingDirectory}${context.isGitRepo() ? `Current branch: ${context.getGitBranch()}` : ""}Write clean, maintainable code.`,);

Append Mode (adds to previous layers):

import{createAppendPrompt}from"@/config/helpers";exportdefaultcreateAppendPrompt((context)=>`Additional instructions for this preset.`,);

You can also use Markdown files in append mode, just name them: <target>.append.md

User Prompt (CLAUDE.md)

Project-specific instructions and context. See config/global/prompts/user.ts for a full example:

import{createPrompt}from"@/config/helpers";exportdefaultcreatePrompt((context)=>`# CRITICAL RULESDo exactly what the user asks. No alternatives, no "better" solutions...Working in: ${context.workingDirectory}Git branch: ${context.getGitBranch()}`,);

Commands

Custom slash commands available in Claude. See config/global/commands/ for examples:

Static (Markdown) (config/global/commands/review.md):

# Review
Review: "$ARGUMENTS"
You are conducting a code review...

Dynamic (TypeScript):

import{createCommand}from"@/config/helpers";exportdefaultcreateCommand((context)=>`# Custom CommandWorking in ${context.workingDirectory}Current branch: ${context.getGitBranch()}Your command instructions here...`,);

Append to existing command:

import{createAppendCommand}from"@/config/helpers";exportdefaultcreateAppendCommand((context)=>`Additional instructions for TypeScript projects...`,);

Skills

Skills are reusable instruction bundles that Claude can invoke via the Skill tool. Define them as folders under skills/ with either SKILL.md (static) or SKILL.ts (structured) and any supporting files (e.g. references/*.md).

Static (Markdown) (config/global/skills/my-skill/SKILL.md):

---name: my-skilldescription: Quick checks for the current repoallowed-tools:
- Read
- Grep---
Use this skill to run quick repository checks and summarize findings.

Structured (TypeScript) (config/global/skills/my-skill/SKILL.ts):

import{createSkill}from"@/config/helpers";exportdefaultcreateSkill((context)=>({description: `Checks for ${context.project.name}`,content: `Run targeted analysis for ${context.workingDirectory}.Use $ARGUMENTS to accept parameters.`,allowedTools: ["Read","Grep"],userInvocable: true,disableModelInvocation: false,hooks: {PreToolUse: [{matcher: "Bash",hooks: [{type: "command",command: "echo 'Skill hook'"}],},],},}));

Skill layers are resolved global, then preset, then project. A structured skill may set mode: "append" to append its SKILL.md body to an earlier skill with the same name while keeping the base skill metadata; omitted mode means override.

Hooks

Event handlers that run at specific Claude events. See config/global/hooks.ts for examples:

Examples for global hooks:

importpfrom"picocolors";import{createHook}from"@/hooks/hook-generator";import{createConfigHooks}from"@/config/helpers";constbashDenyList=[{match: /^\bgit\bcheckout/,message: "You are not allowed to do checkouts or resets",},{match: /^\bgrep\b(?!.*\|)/,message: "Use 'rg' (ripgrep) instead of 'grep' for better performance",},];constsessionStartHook=createHook({event: "SessionStart",id: "global-session-start",handler: (input)=>{consttimestamp=newDate().toISOString();console.log(p.dim("🞄"));console.log(`🚀 Session started from ${p.yellow(input.source)} at ${p.blue(timestamp)}`,);console.log(`📍 Working directory: ${p.yellow(process.cwd())}`);console.log(`🔧 Node version: ${p.yellow(process.version)}`);console.log(p.dim("🞄"));},});constpreBashValidationHook=createHook({event: "PreToolUse",id: "bash-deny-list",handler: (input)=>{constcommand=input.tool_input.commandasstring;if(input.tool_name!=="Bash"||!command)return;constfirstMatchingRule=bashDenyList.find((rule)=>command.match(rule.match),);if(!firstMatchingRule)return;return{continue: true,decision: "block",reason: firstMatchingRule?.message,};},});exportdefaultcreateConfigHooks({SessionStart: [{hooks: [sessionStartHook]}],PreToolUse: [{hooks: [preBashValidationHook]}],});

Hook Options:

constoneTimeHook=createHook({event: "SessionStart",id: "one-time-setup",handler: (input)=>{// this will only run once per sessionconsole.log("First session start only");},timeout: 5,// optional: timeout in secondsonce: true,// optional: run only first time per session});

TypeScript Validation Example (config/presets/typescript/hooks.ts):

import{$}from"zx";import{createHook}from"@/hooks/hook-generator";import{createConfigHooks}from"@/config/helpers";exportdefaultcreateConfigHooks({Stop: [{hooks: [createHook({event: "Stop",id: "typescript-validation",handler: async()=>{constresult=await$`tsc --noEmit`;if(result.exitCode!==0){return{continue: true,decision: "block",reason: `Failed tsc --noEmit:\n${result.text()}`,};}return{suppressOutput: true};},}),],},],});

Agents

Specialized sub-agents for specific tasks. See config/global/agents/ for examples:

Static (Markdown) (config/global/agents/code-reviewer.md):

---name: code-reviewerdescription: Reviews code for quality and best practicestools: [Read, Grep, Glob, Bash]---# Code Reviewer Agent
You are a specialized code review agent conducting **SYSTEMATIC, EVIDENCE-FIRST CODE REVIEWS**.
## Core Principles**EVIDENCE BEFORE OPINION** - Always provide file:line references...

Dynamic (TypeScript):

import{createAgent}from"@/config/helpers";exportdefaultcreateAgent((context)=>`---name: debuggerdescription: Debug issues in ${context.project.name}tools: [Read, Edit, Bash, Grep, Glob]---# Debugger AgentYou are debugging code in ${context.workingDirectory}Current branch: ${context.getGitBranch()}`,);

MCPs

Model Context Protocol servers for extending Claude's capabilities. See config/global/mcps/ for examples:

External MCPs

import{createConfigMCPs}from"@/config/helpers";exportdefaultcreateConfigMCPs({filesystem: {command: "npx",args: ["@modelcontextprotocol/server-filesystem"],env: {FS_ROOT: "/home/user"},},});

Filtering MCP Tools

You can filter which tools are exposed from an external MCP:

import{createConfigMCPs}from"@/config/helpers";exportdefaultcreateConfigMCPs({nixos: {command: "nix",args: ["run","github:utensils/mcp-nixos","--"],filter: (tool)=>{// Exclude specific toolsreturntool.name!=="nixos_search";},},});

The filter function receives a tool object with name and description properties. Return true to include the tool, false to exclude it.

Custom MCPs

You can easily define custom MCPs in your config using FastMCP.

import{FastMCP}from"fastmcp";import{z}from"zod";import{createConfigMCPs,createMCP}from"@/config/helpers";constcustomTools=createMCP((context)=>{constserver=newFastMCP({name: "custom-tools",version: "1.0.0",});server.addTool({name: "getProjectInfo",description: "Get current project information",parameters: z.object({}),execute: async()=>{returnJSON.stringify({directory: context.workingDirectory,branch: context.getGitBranch(),isGitRepo: context.isGitRepo(),},null,2,);},});returnserver;});exportdefaultcreateConfigMCPs({"custom-tools": customTools,});

Plugins

CCC configures Claude Code plugins via layered plugins.ts files (global/preset/project). Claude plugin settings live under the claude namespace.

Workflow

  1. Install plugins using the /plugin command
  2. Find plugin keys in ~/.claude/plugins/installed_plugins.json
  3. Enable plugins in your plugins.ts layer

Enabling Claude Plugins

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({claude: {enabledPlugins: {// Use keys from ~/.claude/plugins/installed_plugins.json"typescript-lsp@claude-plugins-official": true,"gopls-lsp@claude-plugins-official": true,},},});

Local Plugin Directories

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({claude: {pluginDirs: ["./claude-plugins/my-plugin",],},});

You can also drop local plugins into config/claude-plugins/ and CCC will auto-discover them. CLI override: ccc --plugin-dir ./path/to/plugin

LSP Plugin Support

LSP plugins are supported natively. Just enable your LSP plugins normally:

exportdefaultcreateConfigPlugins({claude: {enabledPlugins: {"typescript-lsp@claude-plugins-official": true,},},});

CCC Plugins

CCC has its own plugin system for bundling reusable configuration components. Unlike Claude's built-in plugins (configured via plugins.ts under claude), CCC plugins are local TypeScript modules that can define commands, agents, MCPs, hooks, and prompts dynamically, and which have access to enriched information about the current session.

CCC Plugins vs Claude Plugins

AspectCCC Plugins (plugins.tsccc)Claude Plugins (plugins.tsclaude)
Locationconfig/plugins/ directory~/.claude/plugins/ (installed) or local roots via config/claude-plugins/
FormatTypeScript with createPlugin()Claude's plugin format
Componentscommands, agents, MCPs, hooks, promptsDefined by Claude
DistributionLocal to your configVia /plugin command

Plugin Structure

A CCC plugin requires two files:

config/plugins/my-plugin/
├── plugin.json # Manifest with name, version, description
└── index.ts # Plugin definition using createPlugin()

plugin.json (manifest):

{
"name": "my-plugin",
"version": "1.0.0",
"description": "A custom CCC plugin"
}

index.ts (definition):

import{createPlugin}from"@/config/helpers";exportdefaultcreatePlugin({// Commands - custom slash commandscommands: (context)=>({"my-command": {content: `# My CommandYour command instructions here... `.trim(),mode: "override",},}),// Agents - specialized sub-agentsagents: (context)=>({"my-agent": {content: `---name: my-agentdescription: A specialized agenttools: [Read, Grep, Glob]---Agent instructions... `.trim(),mode: "override",},}),// MCPs - inline MCP serversmcps: (context)=>({"my-mcp": {type: "inline",config: ()=>createMyMCP(context),},}),// Hooks - event handlershooks: (context)=>({Stop: [{hooks: [createHook({event: "Stop",id: "plugin-stop-hook",handler: ()=>{// Hook logic},}),],},],}),// Prompts - system/user prompt additionsprompts: (context)=>({user: {content: "Additional user prompt content",mode: "append",},}),});

Enabling CCC Plugins

Enable CCC plugins via plugins.ts:

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"my-plugin": true,// Enable plugin"another-plugin": false,// Explicitly disable},});

You can also enable plugins in presets:

// config/presets/typescript/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"typescript-helpers": true,},});

Plugin Context

Plugins receive a PluginContext with access to:

{// Standard context properties
workingDirectory: string;
launcherDirectory: string;isGitRepo(): boolean;getGitBranch(): string;// ... all Context methods// Plugin-specific
manifest: PluginManifest;// Plugin's plugin.json
root: string;// Plugin directory path
settings: Record<string,unknown>;// Plugin settings// State management
state: {get<T>(key: string): T|undefined;set(key: string,value: unknown): void;clear(): void;getAll(): Record<string,unknown>;};}

Plugin State

Plugins can persist state using the built-in state API. State is automatically saved to disk and restored between sessions.

State API:

context.state.get<T>(key: string): T|undefined// Get a valuecontext.state.set(key: string,value: unknown): void// Set a valuecontext.state.clear(): void// Clear all statecontext.state.getAll(): Record<string,unknown>// Get all state

State Locations:

By default, plugin state is stored in /tmp/ccc-plugin-{name}-{sessionId}.json. The location is isolated by session ID (CCC_INSTANCE_ID) to prevent conflicts between concurrent CCC instances.

LocationPathUse Case
temp (default)/tmp/ccc-plugin-{name}-{sessionId}.jsonSession-scoped data
project{cwd}/.ccc/state/plugins/{name}.jsonProject-specific persistent data
user~/.ccc/state/plugins/{name}.jsonGlobal user preferences

Example - Stateful MCP:

exportdefaultcreatePlugin({mcps: (context)=>({"stateful-mcp": {type: "inline",config: ()=>{constserver=newFastMCP({name: "stateful",version: "1.0.0"});server.addTool({name: "save_data",parameters: z.object({key: z.string(),value: z.string()}),execute: async(args)=>{context.state.set(args.key,args.value);return"Saved!";},});server.addTool({name: "load_data",parameters: z.object({key: z.string()}),execute: async(args)=>{returncontext.state.get(args.key)??"Not found";},});returnserver;},},}),});

Plugin Settings

Plugins define settings using Zod schemas in index.ts. The type is automatically inferred:

// my-plugin/index.tsimport{z}from"zod";import{createPlugin}from"@/config/helpers";constsettingsSchema=z.object({maxItems: z.number().default(100),mode: z.enum(["fast","balanced","thorough"]).default("balanced"),});exportdefaultcreatePlugin({
settingsSchema,mcps: (context)=>{// context.plugin.settings is typed as { maxItems: number, mode: "fast" | "balanced" | "thorough" }const{ maxItems, mode }=context.plugin.settings;// ...},});

Pass settings when enabling the plugin:

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"my-plugin": {enabled: true,settings: {maxItems: 50,mode: "fast",},},},});

Settings are validated with Zod at plugin load time. Invalid settings throw errors.

Plugin State

Plugins get a context.state API for key-value storage:

context.state.get<T>(key)// get valuecontext.state.set(key,value)// set valuecontext.state.clear()// clear all statecontext.state.getAll()// get all state

By default, state is in-memory only (not persisted). To persist state, set stateType:

exportdefaultcreatePlugin({stateType: "temp",// /tmp/ccc-plugin-{name}-{sessionId}.json (per-instance)// or:stateType: "project",// {projectRoot}/.ccc/state/plugins/{name}.json// or:stateType: "user",// ~/.ccc/state/plugins/{name}.jsonmcps: (context)=>{/* ... */},});
stateTypePathUse case
"none" (default)(in-memory only)No persistence needed
"temp"/tmp/ccc-plugin-{name}-{sessionId}.jsonPer-instance state, cleared on reboot
"project"{projectRoot}/.ccc/state/plugins/{name}.jsonProject-specific persistent state
"user"~/.ccc/state/plugins/{name}.jsonUser-wide persistent state

onLoad Hook

Plugins can define an onLoad callback for initialization:

exportdefaultcreatePlugin({onLoad: async(context)=>{console.log(`Plugin ${context.plugin.name} loaded`);},commands: ()=>({/* ... */}),});

Inter-Plugin Communication

Plugins can access other loaded plugins using getPlugin():

exportdefaultcreatePlugin({mcps: (context)=>({"my-mcp": {type: "inline",config: ()=>{constserver=newFastMCP({name: "my-mcp",version: "1.0.0"});server.addTool({name: "get_other_plugin_data",parameters: z.object({}),execute: async()=>{// Access another plugin's contextconstotherPlugin=context.getPlugin("other-plugin");if(!otherPlugin)return"Other plugin not loaded";// Read its stateconstdata=otherPlugin.state.get("shared-data");returnJSON.stringify(data);},});returnserver;},},}),});

Plugin Dependencies

Plugins can declare dependencies on other plugins in plugin.json:

{
"name": "my-plugin",
"version": "1.0.0",
"description": "Depends on base-plugin",
"dependencies": ["base-plugin", "utility-plugin"]
}

Dependencies are loaded first, ensuring they're available when your plugin loads.

Full plugin.json Schema

{
"name": "my-plugin",
"version": "1.0.0",
"description": "A complete example plugin",
"author": "Your Name",
"license": "MIT",
"homepage": "https://example.com/my-plugin",
"repository": "https://github.com/user/my-plugin",
"dependencies": ["other-plugin"]
}

Note: Settings are defined via Zod schema in index.ts, not in plugin.json.

Example: Complete Plugin

Here's a complete example of a plugin with commands, an MCP, and hooks:

// config/plugins/task-tracker/index.tsimport{FastMCP}from"fastmcp";import{z}from"zod";import{createPlugin}from"@/config/helpers";import{createHook}from"@/hooks/hook-generator";exportdefaultcreatePlugin({commands: ()=>({"track": {content: `# Task TrackerTrack a new task: "$ARGUMENTS"Use the task_add MCP tool to add this task. `.trim(),mode: "override",},}),mcps: (context)=>({"task-tracker": {type: "inline",config: ()=>{constserver=newFastMCP({name: "task-tracker",version: "1.0.0",});server.addTool({name: "task_add",description: "Add a new task",parameters: z.object({title: z.string(),priority: z.enum(["low","medium","high"]).default("medium"),}),execute: async(args)=>{consttasks=context.state.get<string[]>("tasks")??[];tasks.push(`[${args.priority}] ${args.title}`);context.state.set("tasks",tasks);return`Added: ${args.title}`;},});server.addTool({name: "task_list",description: "List all tasks",parameters: z.object({}),execute: async()=>{consttasks=context.state.get<string[]>("tasks")??[];returntasks.length>0 ? tasks.join("\n") : "No tasks";},});returnserver;},},}),hooks: ()=>({SessionStart: [{hooks: [createHook({event: "SessionStart",id: "task-tracker-init",handler: ()=>{console.log("📋 Task Tracker plugin loaded");},}),],},],}),});

Viewing Plugin Info

Use ccc --print-config to see loaded CCC plugins:

CCC Plugins:
my-plugin (v1.0.0) [enabled]
Commands: my-plugin:my-command
MCPs: my-plugin:my-mcp
Hooks: Stop(1)

Note: Plugin components are namespaced with the plugin name (e.g., my-plugin:my-command).

Runtime Patches

All CLI patches are applied at runtime - the original node_modules files are never modified. The launcher reads the CLI, applies patches, writes to a temp file, and imports that instead.

Built-in Patches

Applied automatically on every launch:

  • Disable pr-comments and security-review features

User-defined Patches

Add custom string replacements via settings:

exportdefaultcreateConfigSettings({patches: [{find: "ultrathink",replace: "uuu"},// shorter alias],});

Patches are applied after built-in patches. No reinstall needed when changing configuration.

Statusline

Customize the Claude statusline with a simple configuration-based approach.

Priority Order

  1. config/global/statusline.ts - If this file exists, it will be executed with bun
  2. settings.statusLine - Otherwise, use the statusLine configuration from settings
  3. None - If neither is configured, no statusline is displayed

Creating a Statusline

Create config/global/statusline.ts:

import{createStatusline}from"@/config/helpers";importtype{StatusLineInput}from"@/types/statusline";exportdefaultcreateStatusline(async(data: StatusLineInput)=>{constmodelIcon=data.model?.id?.includes("opus") ? "🦆" : "🐇";constcomponents=[];// Model and iconcomponents.push(`${modelIcon}${data.model.display_name}`);// Working directoryif(data.workspace){constdir=data.workspace.project_dir||data.workspace.current_dir;constshortDir=dir.split("/").slice(-2).join("/");components.push(`📁 ${shortDir}`);}// Hook event (if present)if(data.hook_event_name){components.push(`⚡ ${data.hook_event_name}`);}console.log(components.join(" │ "));});

Using External Tools

You can also integrate external statusline tools:

import{createStatusline}from"@/config/helpers";import{$}from"bun";exportdefaultcreateStatusline(async(data)=>{// Use external ccstatusline toolconstoutput=await$`echo ${JSON.stringify(data)} | bunx ccstatusline`.text();constmodelIcon=data.model?.id?.includes("opus") ? "🦆" : "🐇";console.log(`${modelIcon}${output.trim()}`);});

StatusLineInput Type

The statusline function receives a StatusLineInput object with:

  • model.id - Model identifier (e.g., "claude-3-opus-20240229")
  • model.display_name - Human-readable model name
  • workspace.current_dir - Current working directory
  • workspace.project_dir - Project root directory
  • hook_event_name - Current hook event being executed
  • session_id - Current session identifier
  • transcript_path - Path to the transcript file
  • cwd - Current working directory
  • output_style - Output style configuration

Settings Configuration

Alternatively, configure a custom statusline command in settings.ts:

exportdefaultcreateConfigSettings({statusLine: {type: "command",command: "/path/to/your/statusline-script",},});

Note: Unlike other configuration types, statuslines do NOT support layering or merging. Only the global configuration or settings are used.

Doctor (Config Inspector)

Use ccc --doctor to print a diagnostic report of your merged configuration without launching Claude:

ccc --doctor
ccc --doctor --json

The report shows:

  • Presets detected and project configuration in use
  • Layering traces (override/append) for system/user prompts
  • Per-command and per-agent layering traces across global/presets/project
  • MCP servers and their transport type

Dump Configuration

Use ccc --dump-config to create a complete dump of the computed configuration that Claude sees:

ccc --dump-config

This creates a .config-dump/{timestamp}/ directory containing:

  • system.md - The actual computed system prompt
  • user.md - The actual computed user prompt
  • commands/ - All command files as Claude sees them
  • agents/ - All agent files as Claude sees them
  • settings.json - The merged settings
  • mcps.json - The computed MCP configurations
  • metadata.json - Context and dump information

This is useful for debugging configuration issues and understanding exactly what Claude sees.

Debug MCPs

Use ccc --debug-mcp <mcp-name> to launch the MCP Inspector for debugging MCP servers:

ccc --debug-mcp filesystem
ccc --debug-mcp custom-tools

This launches the MCP Inspector with your MCP server, allowing you to:

  • View all available tools, resources, and prompts
  • Test tool invocations interactively
  • Inspect request/response payloads
  • Debug filtered MCPs (shows tools after filtering)

Note:

  • Only works with stdio transport MCPs (not HTTP/SSE)
  • Filtered MCPs will show the filtered tools, not the original ones
  • Inline MCPs (created with FastMCP) are supported

Project Configuration

Create a project-specific configuration:

// config/projects/myapp/project.tsexportdefault{name: "myapp",root: "/path/to/myapp",disableParentClaudeMds: false,// optional, will disable Claude's behavior of loading upper CLAUDE.md files};
// config/projects/myapp/settings.tsimport{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({env: {NODE_ENV: "development",API_URL: "http://localhost:3000",},});

Context Object

All dynamic configurations receive a context object with a few utilities:

{
workingDirectory: string;// Current working directory
launcherDirectory: string;// Path to launcher installation
instanceId: string;// Unique instance identifier
project: Project;// Project instance with config
mcpServers?: Record<string,ClaudeMCPConfig>;// Processed MCP configs for this runisGitRepo(): boolean;// Check if in git repositorygetGitBranch(): string;// Current git branchgetGitStatus(): string;// Git status (porcelain)getGitRecentCommits(n): string;// Recent commit historygetDirectoryTree(): string;// Directory structuregetPlatform(): string;// OS platformgetOsVersion(): string;// OS version infogetCurrentDateTime(): string;// ISO timestamphasMCP(name: string): boolean;// True if MCP with name is configured}

Other things

  • ?

License

MIT License. See LICENSE for details.

About

Custom launcher for Claude Code, supporting dynamic prompts, layered configuration and easy custom hooks and MCPs.

Topics

Resources

Stars

19 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

image

CCC is a launcher for Claude Code that lets you configure prompts, commands, agents, hooks, and MCPs from a single place, in a layered way.

What you get

  • Dynamic Configuration: Generate system/user prompts, commands, agents dynamically.
  • Layered config: Merge global configuration with presets and project overrides.
  • Low‑effort extensibility: write hooks & MCPs in TypeScript with tiny helpers.
image

Not affiliated with Anthropic. Uses the official @anthropic-ai/claude-code CLI.

Warning: Not tested on Windows, open an issue if you run into problems.


Getting Started

1. Setup

# clone this repo somewhere you'd like to keep going to edit your configuration
git clone https://github.com/3rd/ccc.git ~/my-claude-launcher
cd~/my-claude-launcher
# install dependencies and link `ccc`
bun install
bun link
# install tsx globally (required for runtime interception)
bun add -g tsx

Note: Claude Code (@anthropic-ai/claude-code) is included as a dependency.
Bun must also be available on PATH at runtime because CCC launches hook and statusline helpers with bun.
To update it to the latest version do a bun update.

2. Customize your config

Your configuration lives in the ./config directory, which includes some examples by default.

~/my-claude-launcher/ # Your copy of this repository
└── config/
├── global/ # Global configuration
│ ├── prompts/ # System (output style) / user (CLAUDE.md) prompts
│ ├── commands/ # Your commands
│ ├── agents/ # Your sub-agents
│ ├── skills/ # Your skills
│ ├── hooks.ts # Your hooks
│ └── mcps.ts # Your MCPs
├── presets/ # Your language/framework/whatever-specific configs
│ └── typescript/ # Example: TypeScript-specific settings
└── projects/ # Your project-specific overrides
└── myapp/ # Example: Settings for your 'myapp' project

Development Mode: If a ./dev-config directory exists, it will be used instead of ./config. This allows you to keep the example configuration in ./config (committed to git) while using ./dev-config for your actual development configuration.

3. Use it

Your workflow:

  1. Edit your config in ~/my-claude-launcher/config/
  2. Run ccc instead of claude from anywhere
  3. Your config is dynamically built and loaded
ccc # wrap and launch claude
ccc --continue # all the arguments you pass will be passed through to claude# except these special cases used for debugging
ccc --doctor
ccc --print-config
ccc --print-system-prompt
ccc --print-user-prompt
ccc --dump-config
ccc --debug-mcp <mcp-name>
ccc --doru # launcher-only flag; place it before Claude args
ccc --doru --continue

ccc --doru runs CCC through npx doru --ui and auto-opens doru's live UI. Treat --doru as a launcher-only leading flag and place it before any Claude args. The first run may download doru, and doru itself requires npx plus Node.js 22+.

Configuration Layers

ccc loads configurations in layers (later overrides earlier):

  1. Globalconfig/global/ - Base configuration for all projects
  2. Presetsconfig/presets/ - Auto-detected based on project type
  3. Projectsconfig/projects/ - Specific project overrides

Each layer can define:

  • settings.ts - Settings that will go into Claude Code's settings.json
  • prompts/user.{md,ts} - User instructions (CLAUDE.md)
  • prompts/system.{md,ts} - Output style
  • commands/*.{md,ts} - Custom slash commands
  • agents/*.{md,ts} - Custom sub-agents
  • skills/*/SKILL.{md,ts} - Custom skills (and supporting files)
  • hooks.ts - Custom hooks
  • mcps.ts - Custom MCPs

How It Works

ccc injects configurations using a virtual filesystem overlay. Your actual Claude installation remains untouched. Configurations are injected at runtime through Node.js module interception.

The launcher:

  1. Discovers and merges configurations from all layers
  2. Generates a vfs with the merged config
  3. Intercepts Node's modules to serve virtual files
  4. Launches Claude with the injected configuration
Global ┐
Preset ├─► merge ─► "virtual overlay" ─► Claude Code
Project ┘

Using Models from Other Vendors

You can configure CCC to use models from other vendors by setting environment variables in your settings.ts. This allows you to use models like GLM, Kimi K2, or Deepseek through their Anthropic-compatible APIs.

Configuration Examples

Add these environment variables to your config/global/settings.ts:

import{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({env: {// GLM 4.5ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",ANTHROPIC_AUTH_TOKEN: "Z_API_KEY",ANTHROPIC_MODEL: "glm-4.5",ANTHROPIC_FAST_MODEL: "glm-4.5-air",// Kimi K2// ANTHROPIC_BASE_URL: "https://api.moonshot.ai/anthropic",// ANTHROPIC_AUTH_TOKEN: "KIMI_API_KEY",// Deepseek// ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",// ANTHROPIC_AUTH_TOKEN: "DEEPSEEK_API_KEY",// ANTHROPIC_MODEL: "deepseek-chat",// ANTHROPIC_FAST_MODEL: "deepseek-chat"}});

Environment Variables

  • ANTHROPIC_BASE_URL - The base URL for the vendor's API endpoint
  • ANTHROPIC_AUTH_TOKEN - Your API key for the vendor
  • ANTHROPIC_MODEL - The main model to use (e.g., "glm-4.5", "deepseek-chat")
  • ANTHROPIC_FAST_MODEL - The model to use for quick operations (optional)

Usage

Once configured, CCC will automatically use the specified vendor's models instead of Anthropic's models. All CCC features like prompts, commands, agents, hooks, and MCPs will continue to work with the alternative models.

Note: Make sure you have the required API keys and that the vendor's API is compatible with Anthropic's API format.

Extra Configuration

Some settings will still be read from your global ~/.claude.json:

# things like these:
claude config set -g autocheckpointingEnabled true
claude config set -g diffTool delta
claude config set -g supervisorMode true
claude config set -g autoCompactEnabled true
claude config set --global preferredNotifChannel terminal_bell
claude config set -g verbose true

CLI argument settings

Some settings are passed directly as CLI arguments to Claude rather than being written to settings.json. These are grouped under settings.cli. See Claude Code CLI Reference for details.

// config/global/settings.tsimport{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({cli: {tools: ["Bash","Read","Edit","Write"],disallowedTools: ["WebSearch","WebFetch"],allowedTools: ["Read","Glob","Grep"],addDir: ["/path/to/shared/libs"],permissionMode: "plan",verbose: true,debug: "api,hooks",chrome: true,ide: true,enableLspLogging: false,agent: "code-reviewer",},// other settings go to settings.json as normalenv: { ... },});

Available CLI-Only Settings

SettingTypeCLI FlagDescription
toolsstring[] | "default"--tools "Tool1,Tool2"Available tools ("default" for all, [] to disable)
disallowedToolsstring[]--disallowedTools "Tool1,Tool2"Tools removed from model context entirely
allowedToolsstring[]--allowedTools "Tool1,Tool2"Tools that execute without permission prompts
addDirstring[]--add-dir pathAdditional directories Claude can access
permissionModeenum--permission-mode modeStart mode: default, acceptEdits, plan, bypassPermissions
verboseboolean--verboseEnable verbose logging
debugboolean | string--debug [filter]Enable debug mode, optionally with category filter
chromeboolean--chrome / --no-chromeEnable/disable Chrome browser integration
ideboolean--ideAuto-connect to IDE on startup
enableLspLoggingboolean--enable-lsp-loggingEnable verbose LSP logging
agentstring--agent nameDefault agent for the session
agentsRecord<string, AgentDef>--agents JSONCustom subagent definitions
forkSessionboolean--fork-sessionCreate new session ID when resuming
fallbackModelstring--fallback-model nameFallback model when primary is overloaded
settingSourcesstring[]--setting-sources "user,project,local"Config sources to use
strictMcpConfigboolean--strict-mcp-configOnly use specified MCP config

CLI Override

CLI arguments provided directly to ccc override the corresponding settings.cli values:

# override disallowedTools from settings
ccc --disallowedTools "WebSearch,WebFetch"# start in plan mode regardless of settings
ccc --permission-mode plan

System & User Prompts

System Prompt (Output Style)

Controls how Claude responds and behaves.

Static (Markdown) (config/global/prompts/system.md):

You are a helpful coding assistant.
Write clean, maintainable code.
Follow best practices.

Dynamic (TypeScript) (config/global/prompts/system.ts):

import{createPrompt}from"@/config/helpers";exportdefaultcreatePrompt((context)=>`You are working in ${context.workingDirectory}${context.isGitRepo() ? `Current branch: ${context.getGitBranch()}` : ""}Write clean, maintainable code.`,);

Append Mode (adds to previous layers):

import{createAppendPrompt}from"@/config/helpers";exportdefaultcreateAppendPrompt((context)=>`Additional instructions for this preset.`,);

You can also use Markdown files in append mode, just name them: <target>.append.md

User Prompt (CLAUDE.md)

Project-specific instructions and context. See config/global/prompts/user.ts for a full example:

import{createPrompt}from"@/config/helpers";exportdefaultcreatePrompt((context)=>`# CRITICAL RULESDo exactly what the user asks. No alternatives, no "better" solutions...Working in: ${context.workingDirectory}Git branch: ${context.getGitBranch()}`,);

Commands

Custom slash commands available in Claude. See config/global/commands/ for examples:

Static (Markdown) (config/global/commands/review.md):

# Review
Review: "$ARGUMENTS"
You are conducting a code review...

Dynamic (TypeScript):

import{createCommand}from"@/config/helpers";exportdefaultcreateCommand((context)=>`# Custom CommandWorking in ${context.workingDirectory}Current branch: ${context.getGitBranch()}Your command instructions here...`,);

Append to existing command:

import{createAppendCommand}from"@/config/helpers";exportdefaultcreateAppendCommand((context)=>`Additional instructions for TypeScript projects...`,);

Skills

Skills are reusable instruction bundles that Claude can invoke via the Skill tool. Define them as folders under skills/ with either SKILL.md (static) or SKILL.ts (structured) and any supporting files (e.g. references/*.md).

Static (Markdown) (config/global/skills/my-skill/SKILL.md):

---name: my-skilldescription: Quick checks for the current repoallowed-tools:
- Read
- Grep---
Use this skill to run quick repository checks and summarize findings.

Structured (TypeScript) (config/global/skills/my-skill/SKILL.ts):

import{createSkill}from"@/config/helpers";exportdefaultcreateSkill((context)=>({description: `Checks for ${context.project.name}`,content: `Run targeted analysis for ${context.workingDirectory}.Use $ARGUMENTS to accept parameters.`,allowedTools: ["Read","Grep"],userInvocable: true,disableModelInvocation: false,hooks: {PreToolUse: [{matcher: "Bash",hooks: [{type: "command",command: "echo 'Skill hook'"}],},],},}));

Skill layers are resolved global, then preset, then project. A structured skill may set mode: "append" to append its SKILL.md body to an earlier skill with the same name while keeping the base skill metadata; omitted mode means override.

Hooks

Event handlers that run at specific Claude events. See config/global/hooks.ts for examples:

Examples for global hooks:

importpfrom"picocolors";import{createHook}from"@/hooks/hook-generator";import{createConfigHooks}from"@/config/helpers";constbashDenyList=[{match: /^\bgit\bcheckout/,message: "You are not allowed to do checkouts or resets",},{match: /^\bgrep\b(?!.*\|)/,message: "Use 'rg' (ripgrep) instead of 'grep' for better performance",},];constsessionStartHook=createHook({event: "SessionStart",id: "global-session-start",handler: (input)=>{consttimestamp=newDate().toISOString();console.log(p.dim("🞄"));console.log(`🚀 Session started from ${p.yellow(input.source)} at ${p.blue(timestamp)}`,);console.log(`📍 Working directory: ${p.yellow(process.cwd())}`);console.log(`🔧 Node version: ${p.yellow(process.version)}`);console.log(p.dim("🞄"));},});constpreBashValidationHook=createHook({event: "PreToolUse",id: "bash-deny-list",handler: (input)=>{constcommand=input.tool_input.commandasstring;if(input.tool_name!=="Bash"||!command)return;constfirstMatchingRule=bashDenyList.find((rule)=>command.match(rule.match),);if(!firstMatchingRule)return;return{continue: true,decision: "block",reason: firstMatchingRule?.message,};},});exportdefaultcreateConfigHooks({SessionStart: [{hooks: [sessionStartHook]}],PreToolUse: [{hooks: [preBashValidationHook]}],});

Hook Options:

constoneTimeHook=createHook({event: "SessionStart",id: "one-time-setup",handler: (input)=>{// this will only run once per sessionconsole.log("First session start only");},timeout: 5,// optional: timeout in secondsonce: true,// optional: run only first time per session});

TypeScript Validation Example (config/presets/typescript/hooks.ts):

import{$}from"zx";import{createHook}from"@/hooks/hook-generator";import{createConfigHooks}from"@/config/helpers";exportdefaultcreateConfigHooks({Stop: [{hooks: [createHook({event: "Stop",id: "typescript-validation",handler: async()=>{constresult=await$`tsc --noEmit`;if(result.exitCode!==0){return{continue: true,decision: "block",reason: `Failed tsc --noEmit:\n${result.text()}`,};}return{suppressOutput: true};},}),],},],});

Agents

Specialized sub-agents for specific tasks. See config/global/agents/ for examples:

Static (Markdown) (config/global/agents/code-reviewer.md):

---name: code-reviewerdescription: Reviews code for quality and best practicestools: [Read, Grep, Glob, Bash]---# Code Reviewer Agent
You are a specialized code review agent conducting **SYSTEMATIC, EVIDENCE-FIRST CODE REVIEWS**.
## Core Principles**EVIDENCE BEFORE OPINION** - Always provide file:line references...

Dynamic (TypeScript):

import{createAgent}from"@/config/helpers";exportdefaultcreateAgent((context)=>`---name: debuggerdescription: Debug issues in ${context.project.name}tools: [Read, Edit, Bash, Grep, Glob]---# Debugger AgentYou are debugging code in ${context.workingDirectory}Current branch: ${context.getGitBranch()}`,);

MCPs

Model Context Protocol servers for extending Claude's capabilities. See config/global/mcps/ for examples:

External MCPs

import{createConfigMCPs}from"@/config/helpers";exportdefaultcreateConfigMCPs({filesystem: {command: "npx",args: ["@modelcontextprotocol/server-filesystem"],env: {FS_ROOT: "/home/user"},},});

Filtering MCP Tools

You can filter which tools are exposed from an external MCP:

import{createConfigMCPs}from"@/config/helpers";exportdefaultcreateConfigMCPs({nixos: {command: "nix",args: ["run","github:utensils/mcp-nixos","--"],filter: (tool)=>{// Exclude specific toolsreturntool.name!=="nixos_search";},},});

The filter function receives a tool object with name and description properties. Return true to include the tool, false to exclude it.

Custom MCPs

You can easily define custom MCPs in your config using FastMCP.

import{FastMCP}from"fastmcp";import{z}from"zod";import{createConfigMCPs,createMCP}from"@/config/helpers";constcustomTools=createMCP((context)=>{constserver=newFastMCP({name: "custom-tools",version: "1.0.0",});server.addTool({name: "getProjectInfo",description: "Get current project information",parameters: z.object({}),execute: async()=>{returnJSON.stringify({directory: context.workingDirectory,branch: context.getGitBranch(),isGitRepo: context.isGitRepo(),},null,2,);},});returnserver;});exportdefaultcreateConfigMCPs({"custom-tools": customTools,});

Plugins

CCC configures Claude Code plugins via layered plugins.ts files (global/preset/project). Claude plugin settings live under the claude namespace.

Workflow

  1. Install plugins using the /plugin command
  2. Find plugin keys in ~/.claude/plugins/installed_plugins.json
  3. Enable plugins in your plugins.ts layer

Enabling Claude Plugins

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({claude: {enabledPlugins: {// Use keys from ~/.claude/plugins/installed_plugins.json"typescript-lsp@claude-plugins-official": true,"gopls-lsp@claude-plugins-official": true,},},});

Local Plugin Directories

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({claude: {pluginDirs: ["./claude-plugins/my-plugin",],},});

You can also drop local plugins into config/claude-plugins/ and CCC will auto-discover them. CLI override: ccc --plugin-dir ./path/to/plugin

LSP Plugin Support

LSP plugins are supported natively. Just enable your LSP plugins normally:

exportdefaultcreateConfigPlugins({claude: {enabledPlugins: {"typescript-lsp@claude-plugins-official": true,},},});

CCC Plugins

CCC has its own plugin system for bundling reusable configuration components. Unlike Claude's built-in plugins (configured via plugins.ts under claude), CCC plugins are local TypeScript modules that can define commands, agents, MCPs, hooks, and prompts dynamically, and which have access to enriched information about the current session.

CCC Plugins vs Claude Plugins

AspectCCC Plugins (plugins.tsccc)Claude Plugins (plugins.tsclaude)
Locationconfig/plugins/ directory~/.claude/plugins/ (installed) or local roots via config/claude-plugins/
FormatTypeScript with createPlugin()Claude's plugin format
Componentscommands, agents, MCPs, hooks, promptsDefined by Claude
DistributionLocal to your configVia /plugin command

Plugin Structure

A CCC plugin requires two files:

config/plugins/my-plugin/
├── plugin.json # Manifest with name, version, description
└── index.ts # Plugin definition using createPlugin()

plugin.json (manifest):

{
"name": "my-plugin",
"version": "1.0.0",
"description": "A custom CCC plugin"
}

index.ts (definition):

import{createPlugin}from"@/config/helpers";exportdefaultcreatePlugin({// Commands - custom slash commandscommands: (context)=>({"my-command": {content: `# My CommandYour command instructions here... `.trim(),mode: "override",},}),// Agents - specialized sub-agentsagents: (context)=>({"my-agent": {content: `---name: my-agentdescription: A specialized agenttools: [Read, Grep, Glob]---Agent instructions... `.trim(),mode: "override",},}),// MCPs - inline MCP serversmcps: (context)=>({"my-mcp": {type: "inline",config: ()=>createMyMCP(context),},}),// Hooks - event handlershooks: (context)=>({Stop: [{hooks: [createHook({event: "Stop",id: "plugin-stop-hook",handler: ()=>{// Hook logic},}),],},],}),// Prompts - system/user prompt additionsprompts: (context)=>({user: {content: "Additional user prompt content",mode: "append",},}),});

Enabling CCC Plugins

Enable CCC plugins via plugins.ts:

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"my-plugin": true,// Enable plugin"another-plugin": false,// Explicitly disable},});

You can also enable plugins in presets:

// config/presets/typescript/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"typescript-helpers": true,},});

Plugin Context

Plugins receive a PluginContext with access to:

{// Standard context properties
workingDirectory: string;
launcherDirectory: string;isGitRepo(): boolean;getGitBranch(): string;// ... all Context methods// Plugin-specific
manifest: PluginManifest;// Plugin's plugin.json
root: string;// Plugin directory path
settings: Record<string,unknown>;// Plugin settings// State management
state: {get<T>(key: string): T|undefined;set(key: string,value: unknown): void;clear(): void;getAll(): Record<string,unknown>;};}

Plugin State

Plugins can persist state using the built-in state API. State is automatically saved to disk and restored between sessions.

State API:

context.state.get<T>(key: string): T|undefined// Get a valuecontext.state.set(key: string,value: unknown): void// Set a valuecontext.state.clear(): void// Clear all statecontext.state.getAll(): Record<string,unknown>// Get all state

State Locations:

By default, plugin state is stored in /tmp/ccc-plugin-{name}-{sessionId}.json. The location is isolated by session ID (CCC_INSTANCE_ID) to prevent conflicts between concurrent CCC instances.

LocationPathUse Case
temp (default)/tmp/ccc-plugin-{name}-{sessionId}.jsonSession-scoped data
project{cwd}/.ccc/state/plugins/{name}.jsonProject-specific persistent data
user~/.ccc/state/plugins/{name}.jsonGlobal user preferences

Example - Stateful MCP:

exportdefaultcreatePlugin({mcps: (context)=>({"stateful-mcp": {type: "inline",config: ()=>{constserver=newFastMCP({name: "stateful",version: "1.0.0"});server.addTool({name: "save_data",parameters: z.object({key: z.string(),value: z.string()}),execute: async(args)=>{context.state.set(args.key,args.value);return"Saved!";},});server.addTool({name: "load_data",parameters: z.object({key: z.string()}),execute: async(args)=>{returncontext.state.get(args.key)??"Not found";},});returnserver;},},}),});

Plugin Settings

Plugins define settings using Zod schemas in index.ts. The type is automatically inferred:

// my-plugin/index.tsimport{z}from"zod";import{createPlugin}from"@/config/helpers";constsettingsSchema=z.object({maxItems: z.number().default(100),mode: z.enum(["fast","balanced","thorough"]).default("balanced"),});exportdefaultcreatePlugin({
settingsSchema,mcps: (context)=>{// context.plugin.settings is typed as { maxItems: number, mode: "fast" | "balanced" | "thorough" }const{ maxItems, mode }=context.plugin.settings;// ...},});

Pass settings when enabling the plugin:

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"my-plugin": {enabled: true,settings: {maxItems: 50,mode: "fast",},},},});

Settings are validated with Zod at plugin load time. Invalid settings throw errors.

Plugin State

Plugins get a context.state API for key-value storage:

context.state.get<T>(key)// get valuecontext.state.set(key,value)// set valuecontext.state.clear()// clear all statecontext.state.getAll()// get all state

By default, state is in-memory only (not persisted). To persist state, set stateType:

exportdefaultcreatePlugin({stateType: "temp",// /tmp/ccc-plugin-{name}-{sessionId}.json (per-instance)// or:stateType: "project",// {projectRoot}/.ccc/state/plugins/{name}.json// or:stateType: "user",// ~/.ccc/state/plugins/{name}.jsonmcps: (context)=>{/* ... */},});
stateTypePathUse case
"none" (default)(in-memory only)No persistence needed
"temp"/tmp/ccc-plugin-{name}-{sessionId}.jsonPer-instance state, cleared on reboot
"project"{projectRoot}/.ccc/state/plugins/{name}.jsonProject-specific persistent state
"user"~/.ccc/state/plugins/{name}.jsonUser-wide persistent state

onLoad Hook

Plugins can define an onLoad callback for initialization:

exportdefaultcreatePlugin({onLoad: async(context)=>{console.log(`Plugin ${context.plugin.name} loaded`);},commands: ()=>({/* ... */}),});

Inter-Plugin Communication

Plugins can access other loaded plugins using getPlugin():

exportdefaultcreatePlugin({mcps: (context)=>({"my-mcp": {type: "inline",config: ()=>{constserver=newFastMCP({name: "my-mcp",version: "1.0.0"});server.addTool({name: "get_other_plugin_data",parameters: z.object({}),execute: async()=>{// Access another plugin's contextconstotherPlugin=context.getPlugin("other-plugin");if(!otherPlugin)return"Other plugin not loaded";// Read its stateconstdata=otherPlugin.state.get("shared-data");returnJSON.stringify(data);},});returnserver;},},}),});

Plugin Dependencies

Plugins can declare dependencies on other plugins in plugin.json:

{
"name": "my-plugin",
"version": "1.0.0",
"description": "Depends on base-plugin",
"dependencies": ["base-plugin", "utility-plugin"]
}

Dependencies are loaded first, ensuring they're available when your plugin loads.

Full plugin.json Schema

{
"name": "my-plugin",
"version": "1.0.0",
"description": "A complete example plugin",
"author": "Your Name",
"license": "MIT",
"homepage": "https://example.com/my-plugin",
"repository": "https://github.com/user/my-plugin",
"dependencies": ["other-plugin"]
}

Note: Settings are defined via Zod schema in index.ts, not in plugin.json.

Example: Complete Plugin

Here's a complete example of a plugin with commands, an MCP, and hooks:

// config/plugins/task-tracker/index.tsimport{FastMCP}from"fastmcp";import{z}from"zod";import{createPlugin}from"@/config/helpers";import{createHook}from"@/hooks/hook-generator";exportdefaultcreatePlugin({commands: ()=>({"track": {content: `# Task TrackerTrack a new task: "$ARGUMENTS"Use the task_add MCP tool to add this task. `.trim(),mode: "override",},}),mcps: (context)=>({"task-tracker": {type: "inline",config: ()=>{constserver=newFastMCP({name: "task-tracker",version: "1.0.0",});server.addTool({name: "task_add",description: "Add a new task",parameters: z.object({title: z.string(),priority: z.enum(["low","medium","high"]).default("medium"),}),execute: async(args)=>{consttasks=context.state.get<string[]>("tasks")??[];tasks.push(`[${args.priority}] ${args.title}`);context.state.set("tasks",tasks);return`Added: ${args.title}`;},});server.addTool({name: "task_list",description: "List all tasks",parameters: z.object({}),execute: async()=>{consttasks=context.state.get<string[]>("tasks")??[];returntasks.length>0 ? tasks.join("\n") : "No tasks";},});returnserver;},},}),hooks: ()=>({SessionStart: [{hooks: [createHook({event: "SessionStart",id: "task-tracker-init",handler: ()=>{console.log("📋 Task Tracker plugin loaded");},}),],},],}),});

Viewing Plugin Info

Use ccc --print-config to see loaded CCC plugins:

CCC Plugins:
my-plugin (v1.0.0) [enabled]
Commands: my-plugin:my-command
MCPs: my-plugin:my-mcp
Hooks: Stop(1)

Note: Plugin components are namespaced with the plugin name (e.g., my-plugin:my-command).

Runtime Patches

All CLI patches are applied at runtime - the original node_modules files are never modified. The launcher reads the CLI, applies patches, writes to a temp file, and imports that instead.

Built-in Patches

Applied automatically on every launch:

  • Disable pr-comments and security-review features

User-defined Patches

Add custom string replacements via settings:

exportdefaultcreateConfigSettings({patches: [{find: "ultrathink",replace: "uuu"},// shorter alias],});

Patches are applied after built-in patches. No reinstall needed when changing configuration.

Statusline

Customize the Claude statusline with a simple configuration-based approach.

Priority Order

  1. config/global/statusline.ts - If this file exists, it will be executed with bun
  2. settings.statusLine - Otherwise, use the statusLine configuration from settings
  3. None - If neither is configured, no statusline is displayed

Creating a Statusline

Create config/global/statusline.ts:

import{createStatusline}from"@/config/helpers";importtype{StatusLineInput}from"@/types/statusline";exportdefaultcreateStatusline(async(data: StatusLineInput)=>{constmodelIcon=data.model?.id?.includes("opus") ? "🦆" : "🐇";constcomponents=[];// Model and iconcomponents.push(`${modelIcon}${data.model.display_name}`);// Working directoryif(data.workspace){constdir=data.workspace.project_dir||data.workspace.current_dir;constshortDir=dir.split("/").slice(-2).join("/");components.push(`📁 ${shortDir}`);}// Hook event (if present)if(data.hook_event_name){components.push(`⚡ ${data.hook_event_name}`);}console.log(components.join(" │ "));});

Using External Tools

You can also integrate external statusline tools:

import{createStatusline}from"@/config/helpers";import{$}from"bun";exportdefaultcreateStatusline(async(data)=>{// Use external ccstatusline toolconstoutput=await$`echo ${JSON.stringify(data)} | bunx ccstatusline`.text();constmodelIcon=data.model?.id?.includes("opus") ? "🦆" : "🐇";console.log(`${modelIcon}${output.trim()}`);});

StatusLineInput Type

The statusline function receives a StatusLineInput object with:

  • model.id - Model identifier (e.g., "claude-3-opus-20240229")
  • model.display_name - Human-readable model name
  • workspace.current_dir - Current working directory
  • workspace.project_dir - Project root directory
  • hook_event_name - Current hook event being executed
  • session_id - Current session identifier
  • transcript_path - Path to the transcript file
  • cwd - Current working directory
  • output_style - Output style configuration

Settings Configuration

Alternatively, configure a custom statusline command in settings.ts:

exportdefaultcreateConfigSettings({statusLine: {type: "command",command: "/path/to/your/statusline-script",},});

Note: Unlike other configuration types, statuslines do NOT support layering or merging. Only the global configuration or settings are used.

Doctor (Config Inspector)

Use ccc --doctor to print a diagnostic report of your merged configuration without launching Claude:

ccc --doctor
ccc --doctor --json

The report shows:

  • Presets detected and project configuration in use
  • Layering traces (override/append) for system/user prompts
  • Per-command and per-agent layering traces across global/presets/project
  • MCP servers and their transport type

Dump Configuration

Use ccc --dump-config to create a complete dump of the computed configuration that Claude sees:

ccc --dump-config

This creates a .config-dump/{timestamp}/ directory containing:

  • system.md - The actual computed system prompt
  • user.md - The actual computed user prompt
  • commands/ - All command files as Claude sees them
  • agents/ - All agent files as Claude sees them
  • settings.json - The merged settings
  • mcps.json - The computed MCP configurations
  • metadata.json - Context and dump information

This is useful for debugging configuration issues and understanding exactly what Claude sees.

Debug MCPs

Use ccc --debug-mcp <mcp-name> to launch the MCP Inspector for debugging MCP servers:

ccc --debug-mcp filesystem
ccc --debug-mcp custom-tools

This launches the MCP Inspector with your MCP server, allowing you to:

  • View all available tools, resources, and prompts
  • Test tool invocations interactively
  • Inspect request/response payloads
  • Debug filtered MCPs (shows tools after filtering)

Note:

  • Only works with stdio transport MCPs (not HTTP/SSE)
  • Filtered MCPs will show the filtered tools, not the original ones
  • Inline MCPs (created with FastMCP) are supported

Project Configuration

Create a project-specific configuration:

// config/projects/myapp/project.tsexportdefault{name: "myapp",root: "/path/to/myapp",disableParentClaudeMds: false,// optional, will disable Claude's behavior of loading upper CLAUDE.md files};
// config/projects/myapp/settings.tsimport{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({env: {NODE_ENV: "development",API_URL: "http://localhost:3000",},});

Context Object

All dynamic configurations receive a context object with a few utilities:

{
workingDirectory: string;// Current working directory
launcherDirectory: string;// Path to launcher installation
instanceId: string;// Unique instance identifier
project: Project;// Project instance with config
mcpServers?: Record<string,ClaudeMCPConfig>;// Processed MCP configs for this runisGitRepo(): boolean;// Check if in git repositorygetGitBranch(): string;// Current git branchgetGitStatus(): string;// Git status (porcelain)getGitRecentCommits(n): string;// Recent commit historygetDirectoryTree(): string;// Directory structuregetPlatform(): string;// OS platformgetOsVersion(): string;// OS version infogetCurrentDateTime(): string;// ISO timestamphasMCP(name: string): boolean;// True if MCP with name is configured}

Other things

  • ?

License

MIT License. See LICENSE for details.

About

Custom launcher for Claude Code, supporting dynamic prompts, layered configuration and easy custom hooks and MCPs.

Topics

Resources

Stars

19 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

image

CCC is a launcher for Claude Code that lets you configure prompts, commands, agents, hooks, and MCPs from a single place, in a layered way.

What you get

  • Dynamic Configuration: Generate system/user prompts, commands, agents dynamically.
  • Layered config: Merge global configuration with presets and project overrides.
  • Low‑effort extensibility: write hooks & MCPs in TypeScript with tiny helpers.
image

Not affiliated with Anthropic. Uses the official @anthropic-ai/claude-code CLI.

Warning: Not tested on Windows, open an issue if you run into problems.


Getting Started

1. Setup

# clone this repo somewhere you'd like to keep going to edit your configuration
git clone https://github.com/3rd/ccc.git ~/my-claude-launcher
cd~/my-claude-launcher
# install dependencies and link `ccc`
bun install
bun link
# install tsx globally (required for runtime interception)
bun add -g tsx

Note: Claude Code (@anthropic-ai/claude-code) is included as a dependency.
Bun must also be available on PATH at runtime because CCC launches hook and statusline helpers with bun.
To update it to the latest version do a bun update.

2. Customize your config

Your configuration lives in the ./config directory, which includes some examples by default.

~/my-claude-launcher/ # Your copy of this repository
└── config/
├── global/ # Global configuration
│ ├── prompts/ # System (output style) / user (CLAUDE.md) prompts
│ ├── commands/ # Your commands
│ ├── agents/ # Your sub-agents
│ ├── skills/ # Your skills
│ ├── hooks.ts # Your hooks
│ └── mcps.ts # Your MCPs
├── presets/ # Your language/framework/whatever-specific configs
│ └── typescript/ # Example: TypeScript-specific settings
└── projects/ # Your project-specific overrides
└── myapp/ # Example: Settings for your 'myapp' project

Development Mode: If a ./dev-config directory exists, it will be used instead of ./config. This allows you to keep the example configuration in ./config (committed to git) while using ./dev-config for your actual development configuration.

3. Use it

Your workflow:

  1. Edit your config in ~/my-claude-launcher/config/
  2. Run ccc instead of claude from anywhere
  3. Your config is dynamically built and loaded
ccc # wrap and launch claude
ccc --continue # all the arguments you pass will be passed through to claude# except these special cases used for debugging
ccc --doctor
ccc --print-config
ccc --print-system-prompt
ccc --print-user-prompt
ccc --dump-config
ccc --debug-mcp <mcp-name>
ccc --doru # launcher-only flag; place it before Claude args
ccc --doru --continue

ccc --doru runs CCC through npx doru --ui and auto-opens doru's live UI. Treat --doru as a launcher-only leading flag and place it before any Claude args. The first run may download doru, and doru itself requires npx plus Node.js 22+.

Configuration Layers

ccc loads configurations in layers (later overrides earlier):

  1. Globalconfig/global/ - Base configuration for all projects
  2. Presetsconfig/presets/ - Auto-detected based on project type
  3. Projectsconfig/projects/ - Specific project overrides

Each layer can define:

  • settings.ts - Settings that will go into Claude Code's settings.json
  • prompts/user.{md,ts} - User instructions (CLAUDE.md)
  • prompts/system.{md,ts} - Output style
  • commands/*.{md,ts} - Custom slash commands
  • agents/*.{md,ts} - Custom sub-agents
  • skills/*/SKILL.{md,ts} - Custom skills (and supporting files)
  • hooks.ts - Custom hooks
  • mcps.ts - Custom MCPs

How It Works

ccc injects configurations using a virtual filesystem overlay. Your actual Claude installation remains untouched. Configurations are injected at runtime through Node.js module interception.

The launcher:

  1. Discovers and merges configurations from all layers
  2. Generates a vfs with the merged config
  3. Intercepts Node's modules to serve virtual files
  4. Launches Claude with the injected configuration
Global ┐
Preset ├─► merge ─► "virtual overlay" ─► Claude Code
Project ┘

Using Models from Other Vendors

You can configure CCC to use models from other vendors by setting environment variables in your settings.ts. This allows you to use models like GLM, Kimi K2, or Deepseek through their Anthropic-compatible APIs.

Configuration Examples

Add these environment variables to your config/global/settings.ts:

import{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({env: {// GLM 4.5ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",ANTHROPIC_AUTH_TOKEN: "Z_API_KEY",ANTHROPIC_MODEL: "glm-4.5",ANTHROPIC_FAST_MODEL: "glm-4.5-air",// Kimi K2// ANTHROPIC_BASE_URL: "https://api.moonshot.ai/anthropic",// ANTHROPIC_AUTH_TOKEN: "KIMI_API_KEY",// Deepseek// ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",// ANTHROPIC_AUTH_TOKEN: "DEEPSEEK_API_KEY",// ANTHROPIC_MODEL: "deepseek-chat",// ANTHROPIC_FAST_MODEL: "deepseek-chat"}});

Environment Variables

  • ANTHROPIC_BASE_URL - The base URL for the vendor's API endpoint
  • ANTHROPIC_AUTH_TOKEN - Your API key for the vendor
  • ANTHROPIC_MODEL - The main model to use (e.g., "glm-4.5", "deepseek-chat")
  • ANTHROPIC_FAST_MODEL - The model to use for quick operations (optional)

Usage

Once configured, CCC will automatically use the specified vendor's models instead of Anthropic's models. All CCC features like prompts, commands, agents, hooks, and MCPs will continue to work with the alternative models.

Note: Make sure you have the required API keys and that the vendor's API is compatible with Anthropic's API format.

Extra Configuration

Some settings will still be read from your global ~/.claude.json:

# things like these:
claude config set -g autocheckpointingEnabled true
claude config set -g diffTool delta
claude config set -g supervisorMode true
claude config set -g autoCompactEnabled true
claude config set --global preferredNotifChannel terminal_bell
claude config set -g verbose true

CLI argument settings

Some settings are passed directly as CLI arguments to Claude rather than being written to settings.json. These are grouped under settings.cli. See Claude Code CLI Reference for details.

// config/global/settings.tsimport{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({cli: {tools: ["Bash","Read","Edit","Write"],disallowedTools: ["WebSearch","WebFetch"],allowedTools: ["Read","Glob","Grep"],addDir: ["/path/to/shared/libs"],permissionMode: "plan",verbose: true,debug: "api,hooks",chrome: true,ide: true,enableLspLogging: false,agent: "code-reviewer",},// other settings go to settings.json as normalenv: { ... },});

Available CLI-Only Settings

SettingTypeCLI FlagDescription
toolsstring[] | "default"--tools "Tool1,Tool2"Available tools ("default" for all, [] to disable)
disallowedToolsstring[]--disallowedTools "Tool1,Tool2"Tools removed from model context entirely
allowedToolsstring[]--allowedTools "Tool1,Tool2"Tools that execute without permission prompts
addDirstring[]--add-dir pathAdditional directories Claude can access
permissionModeenum--permission-mode modeStart mode: default, acceptEdits, plan, bypassPermissions
verboseboolean--verboseEnable verbose logging
debugboolean | string--debug [filter]Enable debug mode, optionally with category filter
chromeboolean--chrome / --no-chromeEnable/disable Chrome browser integration
ideboolean--ideAuto-connect to IDE on startup
enableLspLoggingboolean--enable-lsp-loggingEnable verbose LSP logging
agentstring--agent nameDefault agent for the session
agentsRecord<string, AgentDef>--agents JSONCustom subagent definitions
forkSessionboolean--fork-sessionCreate new session ID when resuming
fallbackModelstring--fallback-model nameFallback model when primary is overloaded
settingSourcesstring[]--setting-sources "user,project,local"Config sources to use
strictMcpConfigboolean--strict-mcp-configOnly use specified MCP config

CLI Override

CLI arguments provided directly to ccc override the corresponding settings.cli values:

# override disallowedTools from settings
ccc --disallowedTools "WebSearch,WebFetch"# start in plan mode regardless of settings
ccc --permission-mode plan

System & User Prompts

System Prompt (Output Style)

Controls how Claude responds and behaves.

Static (Markdown) (config/global/prompts/system.md):

You are a helpful coding assistant.
Write clean, maintainable code.
Follow best practices.

Dynamic (TypeScript) (config/global/prompts/system.ts):

import{createPrompt}from"@/config/helpers";exportdefaultcreatePrompt((context)=>`You are working in ${context.workingDirectory}${context.isGitRepo() ? `Current branch: ${context.getGitBranch()}` : ""}Write clean, maintainable code.`,);

Append Mode (adds to previous layers):

import{createAppendPrompt}from"@/config/helpers";exportdefaultcreateAppendPrompt((context)=>`Additional instructions for this preset.`,);

You can also use Markdown files in append mode, just name them: <target>.append.md

User Prompt (CLAUDE.md)

Project-specific instructions and context. See config/global/prompts/user.ts for a full example:

import{createPrompt}from"@/config/helpers";exportdefaultcreatePrompt((context)=>`# CRITICAL RULESDo exactly what the user asks. No alternatives, no "better" solutions...Working in: ${context.workingDirectory}Git branch: ${context.getGitBranch()}`,);

Commands

Custom slash commands available in Claude. See config/global/commands/ for examples:

Static (Markdown) (config/global/commands/review.md):

# Review
Review: "$ARGUMENTS"
You are conducting a code review...

Dynamic (TypeScript):

import{createCommand}from"@/config/helpers";exportdefaultcreateCommand((context)=>`# Custom CommandWorking in ${context.workingDirectory}Current branch: ${context.getGitBranch()}Your command instructions here...`,);

Append to existing command:

import{createAppendCommand}from"@/config/helpers";exportdefaultcreateAppendCommand((context)=>`Additional instructions for TypeScript projects...`,);

Skills

Skills are reusable instruction bundles that Claude can invoke via the Skill tool. Define them as folders under skills/ with either SKILL.md (static) or SKILL.ts (structured) and any supporting files (e.g. references/*.md).

Static (Markdown) (config/global/skills/my-skill/SKILL.md):

---name: my-skilldescription: Quick checks for the current repoallowed-tools:
- Read
- Grep---
Use this skill to run quick repository checks and summarize findings.

Structured (TypeScript) (config/global/skills/my-skill/SKILL.ts):

import{createSkill}from"@/config/helpers";exportdefaultcreateSkill((context)=>({description: `Checks for ${context.project.name}`,content: `Run targeted analysis for ${context.workingDirectory}.Use $ARGUMENTS to accept parameters.`,allowedTools: ["Read","Grep"],userInvocable: true,disableModelInvocation: false,hooks: {PreToolUse: [{matcher: "Bash",hooks: [{type: "command",command: "echo 'Skill hook'"}],},],},}));

Skill layers are resolved global, then preset, then project. A structured skill may set mode: "append" to append its SKILL.md body to an earlier skill with the same name while keeping the base skill metadata; omitted mode means override.

Hooks

Event handlers that run at specific Claude events. See config/global/hooks.ts for examples:

Examples for global hooks:

importpfrom"picocolors";import{createHook}from"@/hooks/hook-generator";import{createConfigHooks}from"@/config/helpers";constbashDenyList=[{match: /^\bgit\bcheckout/,message: "You are not allowed to do checkouts or resets",},{match: /^\bgrep\b(?!.*\|)/,message: "Use 'rg' (ripgrep) instead of 'grep' for better performance",},];constsessionStartHook=createHook({event: "SessionStart",id: "global-session-start",handler: (input)=>{consttimestamp=newDate().toISOString();console.log(p.dim("🞄"));console.log(`🚀 Session started from ${p.yellow(input.source)} at ${p.blue(timestamp)}`,);console.log(`📍 Working directory: ${p.yellow(process.cwd())}`);console.log(`🔧 Node version: ${p.yellow(process.version)}`);console.log(p.dim("🞄"));},});constpreBashValidationHook=createHook({event: "PreToolUse",id: "bash-deny-list",handler: (input)=>{constcommand=input.tool_input.commandasstring;if(input.tool_name!=="Bash"||!command)return;constfirstMatchingRule=bashDenyList.find((rule)=>command.match(rule.match),);if(!firstMatchingRule)return;return{continue: true,decision: "block",reason: firstMatchingRule?.message,};},});exportdefaultcreateConfigHooks({SessionStart: [{hooks: [sessionStartHook]}],PreToolUse: [{hooks: [preBashValidationHook]}],});

Hook Options:

constoneTimeHook=createHook({event: "SessionStart",id: "one-time-setup",handler: (input)=>{// this will only run once per sessionconsole.log("First session start only");},timeout: 5,// optional: timeout in secondsonce: true,// optional: run only first time per session});

TypeScript Validation Example (config/presets/typescript/hooks.ts):

import{$}from"zx";import{createHook}from"@/hooks/hook-generator";import{createConfigHooks}from"@/config/helpers";exportdefaultcreateConfigHooks({Stop: [{hooks: [createHook({event: "Stop",id: "typescript-validation",handler: async()=>{constresult=await$`tsc --noEmit`;if(result.exitCode!==0){return{continue: true,decision: "block",reason: `Failed tsc --noEmit:\n${result.text()}`,};}return{suppressOutput: true};},}),],},],});

Agents

Specialized sub-agents for specific tasks. See config/global/agents/ for examples:

Static (Markdown) (config/global/agents/code-reviewer.md):

---name: code-reviewerdescription: Reviews code for quality and best practicestools: [Read, Grep, Glob, Bash]---# Code Reviewer Agent
You are a specialized code review agent conducting **SYSTEMATIC, EVIDENCE-FIRST CODE REVIEWS**.
## Core Principles**EVIDENCE BEFORE OPINION** - Always provide file:line references...

Dynamic (TypeScript):

import{createAgent}from"@/config/helpers";exportdefaultcreateAgent((context)=>`---name: debuggerdescription: Debug issues in ${context.project.name}tools: [Read, Edit, Bash, Grep, Glob]---# Debugger AgentYou are debugging code in ${context.workingDirectory}Current branch: ${context.getGitBranch()}`,);

MCPs

Model Context Protocol servers for extending Claude's capabilities. See config/global/mcps/ for examples:

External MCPs

import{createConfigMCPs}from"@/config/helpers";exportdefaultcreateConfigMCPs({filesystem: {command: "npx",args: ["@modelcontextprotocol/server-filesystem"],env: {FS_ROOT: "/home/user"},},});

Filtering MCP Tools

You can filter which tools are exposed from an external MCP:

import{createConfigMCPs}from"@/config/helpers";exportdefaultcreateConfigMCPs({nixos: {command: "nix",args: ["run","github:utensils/mcp-nixos","--"],filter: (tool)=>{// Exclude specific toolsreturntool.name!=="nixos_search";},},});

The filter function receives a tool object with name and description properties. Return true to include the tool, false to exclude it.

Custom MCPs

You can easily define custom MCPs in your config using FastMCP.

import{FastMCP}from"fastmcp";import{z}from"zod";import{createConfigMCPs,createMCP}from"@/config/helpers";constcustomTools=createMCP((context)=>{constserver=newFastMCP({name: "custom-tools",version: "1.0.0",});server.addTool({name: "getProjectInfo",description: "Get current project information",parameters: z.object({}),execute: async()=>{returnJSON.stringify({directory: context.workingDirectory,branch: context.getGitBranch(),isGitRepo: context.isGitRepo(),},null,2,);},});returnserver;});exportdefaultcreateConfigMCPs({"custom-tools": customTools,});

Plugins

CCC configures Claude Code plugins via layered plugins.ts files (global/preset/project). Claude plugin settings live under the claude namespace.

Workflow

  1. Install plugins using the /plugin command
  2. Find plugin keys in ~/.claude/plugins/installed_plugins.json
  3. Enable plugins in your plugins.ts layer

Enabling Claude Plugins

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({claude: {enabledPlugins: {// Use keys from ~/.claude/plugins/installed_plugins.json"typescript-lsp@claude-plugins-official": true,"gopls-lsp@claude-plugins-official": true,},},});

Local Plugin Directories

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({claude: {pluginDirs: ["./claude-plugins/my-plugin",],},});

You can also drop local plugins into config/claude-plugins/ and CCC will auto-discover them. CLI override: ccc --plugin-dir ./path/to/plugin

LSP Plugin Support

LSP plugins are supported natively. Just enable your LSP plugins normally:

exportdefaultcreateConfigPlugins({claude: {enabledPlugins: {"typescript-lsp@claude-plugins-official": true,},},});

CCC Plugins

CCC has its own plugin system for bundling reusable configuration components. Unlike Claude's built-in plugins (configured via plugins.ts under claude), CCC plugins are local TypeScript modules that can define commands, agents, MCPs, hooks, and prompts dynamically, and which have access to enriched information about the current session.

CCC Plugins vs Claude Plugins

AspectCCC Plugins (plugins.tsccc)Claude Plugins (plugins.tsclaude)
Locationconfig/plugins/ directory~/.claude/plugins/ (installed) or local roots via config/claude-plugins/
FormatTypeScript with createPlugin()Claude's plugin format
Componentscommands, agents, MCPs, hooks, promptsDefined by Claude
DistributionLocal to your configVia /plugin command

Plugin Structure

A CCC plugin requires two files:

config/plugins/my-plugin/
├── plugin.json # Manifest with name, version, description
└── index.ts # Plugin definition using createPlugin()

plugin.json (manifest):

{
"name": "my-plugin",
"version": "1.0.0",
"description": "A custom CCC plugin"
}

index.ts (definition):

import{createPlugin}from"@/config/helpers";exportdefaultcreatePlugin({// Commands - custom slash commandscommands: (context)=>({"my-command": {content: `# My CommandYour command instructions here... `.trim(),mode: "override",},}),// Agents - specialized sub-agentsagents: (context)=>({"my-agent": {content: `---name: my-agentdescription: A specialized agenttools: [Read, Grep, Glob]---Agent instructions... `.trim(),mode: "override",},}),// MCPs - inline MCP serversmcps: (context)=>({"my-mcp": {type: "inline",config: ()=>createMyMCP(context),},}),// Hooks - event handlershooks: (context)=>({Stop: [{hooks: [createHook({event: "Stop",id: "plugin-stop-hook",handler: ()=>{// Hook logic},}),],},],}),// Prompts - system/user prompt additionsprompts: (context)=>({user: {content: "Additional user prompt content",mode: "append",},}),});

Enabling CCC Plugins

Enable CCC plugins via plugins.ts:

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"my-plugin": true,// Enable plugin"another-plugin": false,// Explicitly disable},});

You can also enable plugins in presets:

// config/presets/typescript/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"typescript-helpers": true,},});

Plugin Context

Plugins receive a PluginContext with access to:

{// Standard context properties
workingDirectory: string;
launcherDirectory: string;isGitRepo(): boolean;getGitBranch(): string;// ... all Context methods// Plugin-specific
manifest: PluginManifest;// Plugin's plugin.json
root: string;// Plugin directory path
settings: Record<string,unknown>;// Plugin settings// State management
state: {get<T>(key: string): T|undefined;set(key: string,value: unknown): void;clear(): void;getAll(): Record<string,unknown>;};}

Plugin State

Plugins can persist state using the built-in state API. State is automatically saved to disk and restored between sessions.

State API:

context.state.get<T>(key: string): T|undefined// Get a valuecontext.state.set(key: string,value: unknown): void// Set a valuecontext.state.clear(): void// Clear all statecontext.state.getAll(): Record<string,unknown>// Get all state

State Locations:

By default, plugin state is stored in /tmp/ccc-plugin-{name}-{sessionId}.json. The location is isolated by session ID (CCC_INSTANCE_ID) to prevent conflicts between concurrent CCC instances.

LocationPathUse Case
temp (default)/tmp/ccc-plugin-{name}-{sessionId}.jsonSession-scoped data
project{cwd}/.ccc/state/plugins/{name}.jsonProject-specific persistent data
user~/.ccc/state/plugins/{name}.jsonGlobal user preferences

Example - Stateful MCP:

exportdefaultcreatePlugin({mcps: (context)=>({"stateful-mcp": {type: "inline",config: ()=>{constserver=newFastMCP({name: "stateful",version: "1.0.0"});server.addTool({name: "save_data",parameters: z.object({key: z.string(),value: z.string()}),execute: async(args)=>{context.state.set(args.key,args.value);return"Saved!";},});server.addTool({name: "load_data",parameters: z.object({key: z.string()}),execute: async(args)=>{returncontext.state.get(args.key)??"Not found";},});returnserver;},},}),});

Plugin Settings

Plugins define settings using Zod schemas in index.ts. The type is automatically inferred:

// my-plugin/index.tsimport{z}from"zod";import{createPlugin}from"@/config/helpers";constsettingsSchema=z.object({maxItems: z.number().default(100),mode: z.enum(["fast","balanced","thorough"]).default("balanced"),});exportdefaultcreatePlugin({
settingsSchema,mcps: (context)=>{// context.plugin.settings is typed as { maxItems: number, mode: "fast" | "balanced" | "thorough" }const{ maxItems, mode }=context.plugin.settings;// ...},});

Pass settings when enabling the plugin:

// config/global/plugins.tsimport{createConfigPlugins}from"@/config/helpers";exportdefaultcreateConfigPlugins({ccc: {"my-plugin": {enabled: true,settings: {maxItems: 50,mode: "fast",},},},});

Settings are validated with Zod at plugin load time. Invalid settings throw errors.

Plugin State

Plugins get a context.state API for key-value storage:

context.state.get<T>(key)// get valuecontext.state.set(key,value)// set valuecontext.state.clear()// clear all statecontext.state.getAll()// get all state

By default, state is in-memory only (not persisted). To persist state, set stateType:

exportdefaultcreatePlugin({stateType: "temp",// /tmp/ccc-plugin-{name}-{sessionId}.json (per-instance)// or:stateType: "project",// {projectRoot}/.ccc/state/plugins/{name}.json// or:stateType: "user",// ~/.ccc/state/plugins/{name}.jsonmcps: (context)=>{/* ... */},});
stateTypePathUse case
"none" (default)(in-memory only)No persistence needed
"temp"/tmp/ccc-plugin-{name}-{sessionId}.jsonPer-instance state, cleared on reboot
"project"{projectRoot}/.ccc/state/plugins/{name}.jsonProject-specific persistent state
"user"~/.ccc/state/plugins/{name}.jsonUser-wide persistent state

onLoad Hook

Plugins can define an onLoad callback for initialization:

exportdefaultcreatePlugin({onLoad: async(context)=>{console.log(`Plugin ${context.plugin.name} loaded`);},commands: ()=>({/* ... */}),});

Inter-Plugin Communication

Plugins can access other loaded plugins using getPlugin():

exportdefaultcreatePlugin({mcps: (context)=>({"my-mcp": {type: "inline",config: ()=>{constserver=newFastMCP({name: "my-mcp",version: "1.0.0"});server.addTool({name: "get_other_plugin_data",parameters: z.object({}),execute: async()=>{// Access another plugin's contextconstotherPlugin=context.getPlugin("other-plugin");if(!otherPlugin)return"Other plugin not loaded";// Read its stateconstdata=otherPlugin.state.get("shared-data");returnJSON.stringify(data);},});returnserver;},},}),});

Plugin Dependencies

Plugins can declare dependencies on other plugins in plugin.json:

{
"name": "my-plugin",
"version": "1.0.0",
"description": "Depends on base-plugin",
"dependencies": ["base-plugin", "utility-plugin"]
}

Dependencies are loaded first, ensuring they're available when your plugin loads.

Full plugin.json Schema

{
"name": "my-plugin",
"version": "1.0.0",
"description": "A complete example plugin",
"author": "Your Name",
"license": "MIT",
"homepage": "https://example.com/my-plugin",
"repository": "https://github.com/user/my-plugin",
"dependencies": ["other-plugin"]
}

Note: Settings are defined via Zod schema in index.ts, not in plugin.json.

Example: Complete Plugin

Here's a complete example of a plugin with commands, an MCP, and hooks:

// config/plugins/task-tracker/index.tsimport{FastMCP}from"fastmcp";import{z}from"zod";import{createPlugin}from"@/config/helpers";import{createHook}from"@/hooks/hook-generator";exportdefaultcreatePlugin({commands: ()=>({"track": {content: `# Task TrackerTrack a new task: "$ARGUMENTS"Use the task_add MCP tool to add this task. `.trim(),mode: "override",},}),mcps: (context)=>({"task-tracker": {type: "inline",config: ()=>{constserver=newFastMCP({name: "task-tracker",version: "1.0.0",});server.addTool({name: "task_add",description: "Add a new task",parameters: z.object({title: z.string(),priority: z.enum(["low","medium","high"]).default("medium"),}),execute: async(args)=>{consttasks=context.state.get<string[]>("tasks")??[];tasks.push(`[${args.priority}] ${args.title}`);context.state.set("tasks",tasks);return`Added: ${args.title}`;},});server.addTool({name: "task_list",description: "List all tasks",parameters: z.object({}),execute: async()=>{consttasks=context.state.get<string[]>("tasks")??[];returntasks.length>0 ? tasks.join("\n") : "No tasks";},});returnserver;},},}),hooks: ()=>({SessionStart: [{hooks: [createHook({event: "SessionStart",id: "task-tracker-init",handler: ()=>{console.log("📋 Task Tracker plugin loaded");},}),],},],}),});

Viewing Plugin Info

Use ccc --print-config to see loaded CCC plugins:

CCC Plugins:
my-plugin (v1.0.0) [enabled]
Commands: my-plugin:my-command
MCPs: my-plugin:my-mcp
Hooks: Stop(1)

Note: Plugin components are namespaced with the plugin name (e.g., my-plugin:my-command).

Runtime Patches

All CLI patches are applied at runtime - the original node_modules files are never modified. The launcher reads the CLI, applies patches, writes to a temp file, and imports that instead.

Built-in Patches

Applied automatically on every launch:

  • Disable pr-comments and security-review features

User-defined Patches

Add custom string replacements via settings:

exportdefaultcreateConfigSettings({patches: [{find: "ultrathink",replace: "uuu"},// shorter alias],});

Patches are applied after built-in patches. No reinstall needed when changing configuration.

Statusline

Customize the Claude statusline with a simple configuration-based approach.

Priority Order

  1. config/global/statusline.ts - If this file exists, it will be executed with bun
  2. settings.statusLine - Otherwise, use the statusLine configuration from settings
  3. None - If neither is configured, no statusline is displayed

Creating a Statusline

Create config/global/statusline.ts:

import{createStatusline}from"@/config/helpers";importtype{StatusLineInput}from"@/types/statusline";exportdefaultcreateStatusline(async(data: StatusLineInput)=>{constmodelIcon=data.model?.id?.includes("opus") ? "🦆" : "🐇";constcomponents=[];// Model and iconcomponents.push(`${modelIcon}${data.model.display_name}`);// Working directoryif(data.workspace){constdir=data.workspace.project_dir||data.workspace.current_dir;constshortDir=dir.split("/").slice(-2).join("/");components.push(`📁 ${shortDir}`);}// Hook event (if present)if(data.hook_event_name){components.push(`⚡ ${data.hook_event_name}`);}console.log(components.join(" │ "));});

Using External Tools

You can also integrate external statusline tools:

import{createStatusline}from"@/config/helpers";import{$}from"bun";exportdefaultcreateStatusline(async(data)=>{// Use external ccstatusline toolconstoutput=await$`echo ${JSON.stringify(data)} | bunx ccstatusline`.text();constmodelIcon=data.model?.id?.includes("opus") ? "🦆" : "🐇";console.log(`${modelIcon}${output.trim()}`);});

StatusLineInput Type

The statusline function receives a StatusLineInput object with:

  • model.id - Model identifier (e.g., "claude-3-opus-20240229")
  • model.display_name - Human-readable model name
  • workspace.current_dir - Current working directory
  • workspace.project_dir - Project root directory
  • hook_event_name - Current hook event being executed
  • session_id - Current session identifier
  • transcript_path - Path to the transcript file
  • cwd - Current working directory
  • output_style - Output style configuration

Settings Configuration

Alternatively, configure a custom statusline command in settings.ts:

exportdefaultcreateConfigSettings({statusLine: {type: "command",command: "/path/to/your/statusline-script",},});

Note: Unlike other configuration types, statuslines do NOT support layering or merging. Only the global configuration or settings are used.

Doctor (Config Inspector)

Use ccc --doctor to print a diagnostic report of your merged configuration without launching Claude:

ccc --doctor
ccc --doctor --json

The report shows:

  • Presets detected and project configuration in use
  • Layering traces (override/append) for system/user prompts
  • Per-command and per-agent layering traces across global/presets/project
  • MCP servers and their transport type

Dump Configuration

Use ccc --dump-config to create a complete dump of the computed configuration that Claude sees:

ccc --dump-config

This creates a .config-dump/{timestamp}/ directory containing:

  • system.md - The actual computed system prompt
  • user.md - The actual computed user prompt
  • commands/ - All command files as Claude sees them
  • agents/ - All agent files as Claude sees them
  • settings.json - The merged settings
  • mcps.json - The computed MCP configurations
  • metadata.json - Context and dump information

This is useful for debugging configuration issues and understanding exactly what Claude sees.

Debug MCPs

Use ccc --debug-mcp <mcp-name> to launch the MCP Inspector for debugging MCP servers:

ccc --debug-mcp filesystem
ccc --debug-mcp custom-tools

This launches the MCP Inspector with your MCP server, allowing you to:

  • View all available tools, resources, and prompts
  • Test tool invocations interactively
  • Inspect request/response payloads
  • Debug filtered MCPs (shows tools after filtering)

Note:

  • Only works with stdio transport MCPs (not HTTP/SSE)
  • Filtered MCPs will show the filtered tools, not the original ones
  • Inline MCPs (created with FastMCP) are supported

Project Configuration

Create a project-specific configuration:

// config/projects/myapp/project.tsexportdefault{name: "myapp",root: "/path/to/myapp",disableParentClaudeMds: false,// optional, will disable Claude's behavior of loading upper CLAUDE.md files};
// config/projects/myapp/settings.tsimport{createConfigSettings}from"@/config/helpers";exportdefaultcreateConfigSettings({env: {NODE_ENV: "development",API_URL: "http://localhost:3000",},});

Context Object

All dynamic configurations receive a context object with a few utilities:

{
workingDirectory: string;// Current working directory
launcherDirectory: string;// Path to launcher installation
instanceId: string;// Unique instance identifier
project: Project;// Project instance with config
mcpServers?: Record<string,ClaudeMCPConfig>;// Processed MCP configs for this runisGitRepo(): boolean;// Check if in git repositorygetGitBranch(): string;// Current git branchgetGitStatus(): string;// Git status (porcelain)getGitRecentCommits(n): string;// Recent commit historygetDirectoryTree(): string;// Directory structuregetPlatform(): string;// OS platformgetOsVersion(): string;// OS version infogetCurrentDateTime(): string;// ISO timestamphasMCP(name: string): boolean;// True if MCP with name is configured}

Other things

  • ?

License

MIT License. See LICENSE for details.

About

Custom launcher for Claude Code, supporting dynamic prompts, layered configuration and easy custom hooks and MCPs.

Topics

Resources

Stars

19 stars

Watchers

0 watching

Forks

Contributors

Languages