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.
Not affiliated with Anthropic. Uses the official
@anthropic-ai/claude-codeCLI.
Warning: Not tested on Windows, open an issue if you run into problems.
# 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 tsxNote: Claude Code (@anthropic-ai/claude-code) is included as a dependency.
To update it to the latest version do a bun update.
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
│ ├── 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.
Your workflow:
- Edit your config in
~/my-claude-launcher/config/ - Run
cccinstead ofclaudefrom anywhere - 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 (they don't launch claude)
ccc --doctor
ccc --print-config
ccc --print-system-prompt
ccc --print-user-prompt
ccc --dump-config
ccc --debug-mcp <mcp-name>ccc loads configurations in layers (later overrides earlier):
- Global →
config/global/- Base configuration for all projects - Presets →
config/presets/- Auto-detected based on project type - Projects →
config/projects/- Specific project overrides
Each layer can define:
settings.ts- Settings that will go into Claude Code'ssettings.jsonprompts/user.{md,ts}- User instructions (CLAUDE.md)prompts/system.{md,ts}- Output stylecommands/*.{md,ts}- Custom slash commandsagents/*.{md,ts}- Custom sub-agentshooks.ts- Custom hooksmcps.ts- Custom MCPs
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:
- Discovers and merges configurations from all layers
- Generates a vfs with the merged config
- Intercepts Node's modules to serve virtual files
- Launches Claude with the injected configuration
Global ┐
Preset ├─► merge ─► "virtual overlay" ─► Claude Code
Project ┘
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.
Add these environment variables to your config/global/settings.ts:
import { createConfigSettings } from "@/config/helpers";
export default createConfigSettings({
env: {
// GLM 4.5
ANTHROPIC_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"
}
});ANTHROPIC_BASE_URL- The base URL for the vendor's API endpointANTHROPIC_AUTH_TOKEN- Your API key for the vendorANTHROPIC_MODEL- The main model to use (e.g., "glm-4.5", "deepseek-chat")ANTHROPIC_FAST_MODEL- The model to use for quick operations (optional)
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.
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 trueControls 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";
export default createPrompt(
(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";
export default createAppendPrompt(
(context) => `
Additional instructions for this preset.
`,
);You can also use Markdown files in append mode, just name them: <target>.append.md
Project-specific instructions and context. See config/global/prompts/user.ts for a full example:
import { createPrompt } from "@/config/helpers";
export default createPrompt(
(context) => `
# CRITICAL RULES
Do exactly what the user asks. No alternatives, no "better" solutions...
Working in: ${context.workingDirectory}
Git branch: ${context.getGitBranch()}
`,
);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";
export default createCommand(
(context) => `
# Custom Command
Working in ${context.workingDirectory}
Current branch: ${context.getGitBranch()}
Your command instructions here...
`,
);Append to existing command:
import { createAppendCommand } from "@/config/helpers";
export default createAppendCommand(
(context) => `
Additional instructions for TypeScript projects...
`,
);Event handlers that run at specific Claude events. See config/global/hooks.ts for examples:
Examples for global hooks:
import p from "picocolors";
import { createHook } from "@/hooks/hook-generator";
import { createConfigHooks } from "@/config/helpers";
const bashDenyList = [
{
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",
},
];
const sessionStartHook = createHook("SessionStart", (input) => {
const timestamp = new Date().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("🞄"));
});
const preBashValidationHook = createHook("PreToolUse", (input) => {
const command = input.tool_input.command as string;
if (input.tool_name !== "Bash" || !command) return;
const firstMatchingRule = bashDenyList.find((rule) =>
command.match(rule.match),
);
if (!firstMatchingRule) return;
return {
continue: true,
decision: "block",
reason: firstMatchingRule?.message,
};
});
export default createConfigHooks({
SessionStart: [{ hooks: [sessionStartHook] }],
PreToolUse: [{ hooks: [preBashValidationHook] }],
});TypeScript Validation Example (config/presets/typescript/hooks.ts):
import { $ } from "zx";
import { createHook } from "@/hooks/hook-generator";
import { createConfigHooks } from "@/config/helpers";
export default createConfigHooks({
Stop: [
{
hooks: [
createHook("Stop", async () => {
const result = await $`tsc --noEmit`;
if (result.exitCode !== 0) {
return {
continue: true,
decision: "block",
reason: `Failed tsc --noEmit:\n${result.text()}`,
};
}
return { suppressOutput: true };
}),
],
},
],
});Specialized sub-agents for specific tasks. See config/global/agents/ for examples:
Static (Markdown) (config/global/agents/code-reviewer.md):
---
name: code-reviewer
description: Reviews code for quality and best practices
tools: [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";
export default createAgent(
(context) => `
---
name: debugger
description: Debug issues in ${context.project.name}
tools: [Read, Edit, Bash, Grep, Glob]
---
# Debugger Agent
You are debugging code in ${context.workingDirectory}
Current branch: ${context.getGitBranch()}
`,
);Model Context Protocol servers for extending Claude's capabilities. See config/global/mcps/ for examples:
import { createConfigMCPs } from "@/config/helpers";
export default createConfigMCPs({
filesystem: {
command: "npx",
args: ["@modelcontextprotocol/server-filesystem"],
env: { FS_ROOT: "/home/user" },
},
});You can filter which tools are exposed from an external MCP:
import { createConfigMCPs } from "@/config/helpers";
export default createConfigMCPs({
nixos: {
command: "nix",
args: ["run", "github:utensils/mcp-nixos", "--"],
filter: (tool) => {
// Exclude specific tools
return tool.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.
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";
const customTools = createMCP((context) => {
const server = new FastMCP({
name: "custom-tools",
version: "1.0.0",
});
server.addTool({
name: "getProjectInfo",
description: "Get current project information",
parameters: z.object({}),
execute: async () => {
return JSON.stringify(
{
directory: context.workingDirectory,
branch: context.getGitBranch(),
isGitRepo: context.isGitRepo(),
},
null,
2,
);
},
});
return server;
});
export default createConfigMCPs({
"custom-tools": customTools,
});Customize the Claude statusline with a simple configuration-based approach.
config/global/statusline.ts- If this file exists, it will be executed withbunsettings.statusLine- Otherwise, use the statusLine configuration from settings- None - If neither is configured, no statusline is displayed
Create config/global/statusline.ts:
import { createStatusline } from "@/config/helpers";
import type { StatusLineInput } from "@/types/statusline";
export default createStatusline(async (data: StatusLineInput) => {
const modelIcon = data.model?.id?.includes("opus") ? "🦆" : "🐇";
const components = [];
// Model and icon
components.push(`${modelIcon} ${data.model.display_name }`);
// Working directory
if (data.workspace) {
const dir = data.workspace.project_dir || data.workspace.current_dir;
const shortDir = 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(" │ "));
});You can also integrate external statusline tools:
import { createStatusline } from "@/config/helpers";
import { $ } from "bun";
export default createStatusline(async (data) => {
// Use external ccstatusline tool
const output = await $`echo ${JSON.stringify(data)} | bunx ccstatusline`.text();
const modelIcon = data.model?.id?.includes("opus") ? "🦆" : "🐇";
console.log(`${modelIcon} ${output.trim()}`);
});The statusline function receives a StatusLineInput object with:
model.id- Model identifier (e.g., "claude-3-opus-20240229")model.display_name- Human-readable model nameworkspace.current_dir- Current working directoryworkspace.project_dir- Project root directoryhook_event_name- Current hook event being executedsession_id- Current session identifiertranscript_path- Path to the transcript filecwd- Current working directoryoutput_style- Output style configuration
Alternatively, configure a custom statusline command in settings.ts:
export default createConfigSettings({
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.
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
Use ccc --dump-config to create a complete dump of the computed configuration that Claude sees:
ccc --dump-configThis creates a .config-dump/{timestamp}/ directory containing:
system.md- The actual computed system promptuser.md- The actual computed user promptcommands/- All command files as Claude sees themagents/- All agent files as Claude sees themsettings.json- The merged settingsmcps.json- The computed MCP configurationsmetadata.json- Context and dump information
This is useful for debugging configuration issues and understanding exactly what Claude sees.
Use ccc --debug-mcp <mcp-name> to launch the MCP Inspector for debugging MCP servers:
ccc --debug-mcp filesystem
ccc --debug-mcp custom-toolsThis 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
Create a project-specific configuration:
// config/projects/myapp/project.ts
export default {
name: "myapp",
root: "/path/to/myapp",
disableParentClaudeMds: false, // optional, will disable Claude's behavior of loading upper CLAUDE.md files
};// config/projects/myapp/settings.ts
import { createConfigSettings } from "@/config/helpers";
export default createConfigSettings({
env: {
NODE_ENV: "development",
API_URL: "http://localhost:3000",
},
});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 run
isGitRepo(): boolean; // Check if in git repository
getGitBranch(): string; // Current git branch
getGitStatus(): string; // Git status (porcelain)
getGitRecentCommits(n): string; // Recent commit history
getDirectoryTree(): string; // Directory structure
getPlatform(): string; // OS platform
getOsVersion(): string; // OS version info
getCurrentDateTime(): string; // ISO timestamp
hasMCP(name: string): boolean; // True if MCP with name is configured
}- ?
MIT License. See LICENSE for details.