Skip to content
/ ccc Public

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

License

Notifications You must be signed in to change notification settings

3rd/ccc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

5 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

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.
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
    β”‚   β”œβ”€β”€ 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 (they don't launch claude)
ccc --doctor
ccc --print-config
ccc --print-system-prompt
ccc --print-user-prompt

Configuration Layers

ccc loads configurations in layers (later overrides earlier):

  1. Global β†’ config/global/ - Base configuration for all projects
  2. Presets β†’ config/presets/ - Auto-detected based on project type
  3. Projects β†’ config/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
  • 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 β”˜

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

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";

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

User Prompt (CLAUDE.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()}
`,
);

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";

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...
`,
);

Hooks

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 };
        }),
      ],
    },
  ],
});

Agents

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()}
`,
);

MCPs

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

External MCPs

import { createConfigMCPs } from "@/config/helpers";

export default createConfigMCPs({
  filesystem: {
    command: "npx",
    args: ["@modelcontextprotocol/server-filesystem"],
    env: { FS_ROOT: "/home/user" },
  },
});

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";

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,
});

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";
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(" β”‚ "));
});

Using External Tools

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()}`);
});

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:

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.

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

Project Configuration

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",
  },
});

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 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
}

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

License

Stars

Watchers

Forks