CLI for managing fine-grained Claude Code configuration per project.
Handles CLAUDE.md composition, plugin enablement, MCP servers, skill overrides, and rules β without manually juggling files.
Linux x86_64 β one line (no sudo required):
curl -fsSL https://raw.githubusercontent.com/aneveux/ccx/master/install.sh | bashThe installer drops the ccx binary into ~/.local/bin by default. Make sure that
directory is on your $PATH:
export PATH="$HOME/.local/bin:$PATH" # add to ~/.bashrc or ~/.zshrcOverride the install directory with CCX_INSTALL_DIR:
CCX_INSTALL_DIR=/usr/local/bin curl -fsSL https://raw.githubusercontent.com/aneveux/ccx/master/install.sh | bashThe installer downloads the latest Linux x86_64 binary, verifies its SHA-256
checksum before installing (fail-closed β it aborts on a missing or mismatched
checksum), then moves the binary into the install directory. Set
CCX_SKIP_CHECKSUM=1 only if you understand the risk. Pin a specific release with
CCX_VERSION (a tag such as v0.6.0; defaults to latest).
Or download a binary directly from Releases.
ccx ships no profiles in the binary β author your own with:
ccx profiles new my-profile
ccx profiles enable my-profilePrerequisites: Java 25, Maven 3.9+
JVM (development):
mvn verify
java -jar target/quarkus-app/quarkus-run.jar --helpNative binary (requires GraalVM 25 with native-image):
mvn package -Pnative
./target/ccx --help| Command | Description |
|---|---|
clean |
Remove ccx-managed config from project (--force) |
doctor |
Validate project configuration |
status |
Show current configuration summary |
plugins |
Enable/disable/list plugins |
profiles |
Manage profiles |
skills |
Manage skill overrides |
mcp |
Configure MCP servers |
claude-md |
Manage CLAUDE.md fragment references |
config |
Manage ccx configuration (global fragments) |
tui |
Interactive terminal dashboard |
Global options. Most commands accept --project-dir (the project to act on,
defaults to cwd). Read-only commands that resolve global config also accept
--global-dir (the Claude config dir, defaults to ~/.claude): plugins list,
plugins diff, plugins tags, plugins refresh, skills list, skills show.
doctor and status also accept --global-dir, but default it to your home
directory (they resolve .claude/settings.json underneath).
doctor additionally probes for git, jq, and yq on your PATH β a missing
git is reported as [WARNING], missing jq/yq as [INFO]. It exits 1 when
any [ERROR] is found, or when git is missing; every other [WARNING] and
[INFO] is advisory and leaves the exit code at 0. In particular a plugin that
resolves against no configured marketplace is a non-failing warning (ADR-015).
| Flag | Command | Description |
|---|---|---|
--full |
status |
Also show global (~/.claude) configuration, not just the project |
--force |
clean |
Actually delete ccx-managed config β without it, clean only prints what it would remove |
clean is a dry run by default: bare ccx clean reports the files and settings
keys it would touch and changes nothing. ccx clean --force is the destructive
form β it strips ccx-managed keys from .claude/settings.json, removes the ccx
marker block from .claude/CLAUDE.md, deletes the rule and command files ccx
installed, and drops .ccx/context.json.
| Subcommand | Description |
|---|---|
profiles list |
List all available profiles |
profiles show <name> |
Show profile details (plugins, rules, env, etc.) |
profiles enable <name> |
Apply a profile to this project |
profiles disable <name> |
Remove a profile from this project |
profiles reinstall <name> |
Re-apply a profile to its declared state |
profiles status |
Show drift state of all profiles |
profiles edit <name> |
Edit a profile's profile.yaml in $EDITOR |
profiles new <name> |
Scaffold a new profile directory |
| Subcommand | Description |
|---|---|
plugins list |
List all known plugins with status (--tag to filter) |
plugins enable <name> |
Enable a plugin in this project (and install it if needed) |
plugins disable <name> |
Disable a plugin |
plugins sync |
Install enabled plugins that are missing locally (--dry-run to preview) |
plugins diff |
Show diff between project and global plugins |
plugins tags |
List available tag filters for plugins list |
plugins refresh |
Force re-fetch GitHub marketplace sources (--marketplace/-m to target one) |
Claude Code keeps two separate pieces of state for a plugin: it is enabled by
enabledPlugins in .claude/settings.json, and it is installed by a record in
~/.claude/plugins/installed_plugins.json plus a materialized tree under
~/.claude/plugins/cache/. Enabled but not installed, Claude Code refuses to load it and
reports isn't installed here or not cached.
ccx writes the enablement and delegates the installation to
claude plugin install <plugin>@<marketplace> --scope project, run in the project directory.
Every enable path does it: plugins enable, profiles enable, profiles reinstall, and the
TUI's plugin and profile toggles (in the background, so the UI never blocks). If the claude
binary cannot be found or the install fails, the enablement is still written and the exact
command to run by hand is printed. ccx doctor reports the gap as an error, and
ccx plugins sync repairs it.
Set CCX_CLAUDE_BIN to point at the claude binary when it is not on PATH.
| Subcommand | Description |
|---|---|
skills list |
List all discovered skills with their status |
skills enable <id> |
Enable a skill at auto (remove its override) |
skills disable <id> |
Disable a skill (off override) |
skills hide <id> |
Set a skill to user-invocable-only |
skills show <id> |
Show the current override status of a skill |
skills reset |
Remove all skill overrides from project settings |
| Subcommand | Description |
|---|---|
mcp list |
List configured MCP servers |
mcp enable <name> |
Add or update an MCP server in settings.json |
mcp disable <name> |
Remove an MCP server from settings.json |
mcp show <name> |
Show MCP server configuration details |
mcp enable adds a server or updates one that already exists. Describe the server
with flags:
# command server (--type defaults to "command")
ccx mcp enable filesystem --command mcp-server-fs --args --root --args .
# HTTP server
ccx mcp enable remote-api --type http --url https://mcp.example.com/sse--args is repeatable β each occurrence adds one argument. The --command,
--url, --args, and --type flags map to the command/url/args/type
fields of the server entry.
Power-user path: pass the full config as a JSON positional instead of flags.
ccx mcp enable filesystem '{"command": "mcp-server-fs", "args": ["--root", "."]}'
ccx mcp enable remote-api '{"type": "http", "url": "https://mcp.example.com/sse"}'Manage references to global CLAUDE.md fragments inside .claude/CLAUDE.md.
| Subcommand | Description |
|---|---|
claude-md list |
List configured fragments in the ccx marker block |
claude-md add <name> |
Add a global fragment reference to .claude/CLAUDE.md |
claude-md remove <name> |
Remove a global fragment reference |
claude-md init |
Initialize .claude/CLAUDE.md with an empty marker block |
A fragment named <name> must already exist at ~/.config/ccx/claudemd/<name>.md
(create one with ccx config claudemd new <name>). claude-md add writes an
absolute @<path> reference into the <!-- ccx:begin -->β¦<!-- ccx:end --> block.
| Subcommand | Description |
|---|---|
config claudemd new <name> |
Create a new global claudemd fragment |
| File | Purpose |
|---|---|
.claude/settings.json |
Plugins, permissions, MCP servers, skill overrides for this project |
.ccx/context.json |
Which profiles are enabled, their installation snapshots, and drift state |
.claude/rules/*.md |
Per-rule markdown files installed by profiles |
.claude/CLAUDE.md |
Project instructions; the <!-- ccx:begin -->β¦<!-- ccx:end --> block is managed by ccx |
~/.config/ccx/profiles/<name>/ |
User-authored profiles β one directory per profile |
~/.config/ccx/claudemd/<name>.md |
Global CLAUDE.md fragments referenced by claude-md add |
The project settings file ccx reads and writes. Only the keys ccx touches are shown; unknown keys are preserved.
{
"enabledPlugins": {
"java@my-marketplace": true
},
"permissions": {
"allow": ["Bash(mvn *)"],
"deny": ["Bash(rm -rf *)"]
},
"mcpServers": {
"filesystem": { "command": "mcp-server-fs", "args": ["--root", "."] }
},
"skillOverrides": {
"java:java": "off"
}
}A profile declares these blocks in its profile.yaml:
permissions:
deny: ["Bash(rm -rf *)"]
env:
JAVA_HOME: /usr/lib/jvm/java-21
ignore:
- "target/"
- "*.class"On ccx profiles enable, all three merge into .claude/settings.json. ignore
patterns are translated into Read(...) deny rules and joined with the profile's
own permissions.deny entries β Claude Code has no separate ignore-file mechanism:
// .claude/settings.json
{
"permissions": {
"deny": ["Bash(rm -rf *)", "Read(target/**)", "Read(**/*.class)"]
},
"env": { "JAVA_HOME": "/usr/lib/jvm/java-21" }
}The translation is purely syntactic:
ignore: entry |
Deny rule written |
|---|---|
target/ |
Read(target/**) |
*.class |
Read(**/*.class) |
.env |
Read(.env) |
docs/*.md |
Read(docs/*.md) |
Read(custom/**) |
Read(custom/**) |
An entry already written as Read(...), Write(...), or Bash(...) passes through
verbatim, so you can bypass the auto-wrapping when you want exact control.
Two things to know about the rules this produces:
- They are unanchored on purpose.
Read(target/**)has no leading/, so Claude Code resolves it relative to the current directory and, for a single-segment directory pattern in a deny rule, matches atargetdirectory at any depth beneath it.Read(/target/**)would pin it to the project root only. Unanchored is the broader deny, which is what you want from an ignore list. WriteRead(/target/**)yourself if you need the narrow form. - They cover reads and edits, not writes. A
Read(...)deny rule blocks the Read and Edit tools on the matched path, and does not apply to a file touched by an arbitrary subprocess (Bash(cat β¦)). Add an explicitWrite(...)orBash(...)deny entry if you need those too.
ccx tracks which profiles are enabled and an installation snapshot per profile so it can detect drift. Written automatically β you do not edit this by hand.
{
"version": "1",
"enabledProfiles": {
"java": {
"enabledAt": "2026-06-24T10:15:30Z",
"snapshot": {
"plugins": ["java@my-marketplace"],
"rules": ["no-empty-catch"],
"permissionsAllow": ["Bash(mvn *)"],
"permissionsDeny": [],
"commands": [],
"fragments": [],
"env": {},
"ignorePatterns": ["target/"]
}
}
}
}ccx ships no profiles in the binary β author your own (see Creating your own profile below).
Once a profile exists under ~/.config/ccx/profiles/, apply it to a project:
# scaffold a profile, then edit ~/.config/ccx/profiles/java/profile.yaml
ccx profiles new java
# apply it to a project (auto-enables any profiles it extends)
ccx profiles enable java --project-dir ~/my-project
# check what was applied
ccx status --project-dir ~/my-project
# launch the interactive dashboard
ccx tui --project-dir ~/my-projectccx ships no profiles in the binary β author your own under
~/.config/ccx/profiles/ β one directory per profile. Scaffold one with:
ccx profiles new my-profile
# edit the generated ~/.config/ccx/profiles/my-profile/profile.yaml~/.config/ccx/
βββ profiles/
β βββ my-profile/
β βββ profile.yaml # the profile definition (see below)
β βββ rules/ # *.md rule files installed into .claude/rules/
β β βββ no-empty-catch.md
β βββ claudemd/ # fragments for the CLAUDE.md block (see claudeMd.fragments)
β β βββ java.md
β βββ commands/ # *.md command files
β βββ ship.md
βββ claudemd/ # global fragments, shared across all profiles
βββ company-style.md
A profile is a named bundle of plugins, rules, permissions, CLAUDE.md fragments, environment variables, and ignore patterns. Every field is optional.
metadata:
name: my-profile
description: "Java + Quarkus working set"
# inherit from other profiles; merged depth-first
extends:
- base
plugins:
enable:
- java@my-marketplace
permissions:
allow:
- "Bash(mvn *)"
deny:
- "Bash(rm -rf *)"
claudeMd:
fragments: # omit the key to inject every *.md in claudemd/
- java # resolves to claudemd/java.md
env:
ENABLE_LSP_TOOL: "1"
ignore:
- "target/"Rule and claudemd fragments are plain markdown. Rule files are auto-discovered:
every *.md in a profile's rules/ directory is applied β there is no key for
them in profile.yaml. Example
~/.config/ccx/profiles/my-profile/claudemd/java.md:
# Java conventions
- Records over POJOs, sealed classes, pattern matching
- SLF4J for logging β never System.out
- Constructor injection, never field injectionFragments that aren't tied to a single profile live in
~/.config/ccx/claudemd/. Create one and reference it from a project:
# create a global fragment
ccx config claudemd new company-style
# reference it from the current project's .claude/CLAUDE.md
ccx claude-md add company-styleclaude-md add writes an absolute @<path> line into the ccx-managed block of
.claude/CLAUDE.md, so Claude Code picks up the fragment for that project.
Profiles are named bundles of plugins, fragments, rules, and permissions. When you enable a profile, ccx:
- Applies plugins to
.claude/settings.json - Writes rule files to
.claude/rules/ - Adds fragment references to the
<!-- ccx:begin -->β¦<!-- ccx:end -->block inCLAUDE.md - Records the installation snapshot in
.ccx/context.json
The snapshot enables drift detection: if you manually remove a plugin or rule file, profiles status will show ALTERED. Use profiles reinstall to bring the profile back to its declared state.
ccx tui [--project-dir .]Interactive 5-tab dashboard (Overview, Profiles, Plugins, MCPs, CLAUDE.md). Requires a real TTY.
| Key | Action |
|---|---|
| Tab / S-Tab | Next / previous tab |
| 1β5 | Jump to tab by number |
| β / β | Navigate list (scroll on Overview) |
j / k |
Scroll down / up (Overview tab) |
/ |
Focus filter bar |
| Esc | Clear filter / blur filter bar |
| Enter | Toggle the selected item β profile, plugin, or CLAUDE.md fragment; removes the server on the MCP tab |
r |
Reinstall profile (Profiles tab) |
r |
Refresh marketplace (Plugins tab) |
q |
Quit |
? |
Toggle help overlay |
mvn verify# Install GraalVM 25 with native-image
mvn package -Pnative
./target/ccx --helpReleases are created by pushing a v* tag. The CI builds a Linux x86_64 native binary and publishes a GitHub Release via JReleaser.
git tag v0.1.0
git push origin v0.1.0How a build becomes an installed binary, end to end.
1. Build β mvn clean package -Pnative produces target/ccx, a
self-contained Linux x86_64 native executable (no JVM required at runtime).
2. Release β the release Maven profile runs JReleaser, which publishes the
native binary to GitHub Releases on the
aneveux/ccx repository with a conventional-commits changelog. The release is cut
by pushing a v* tag (see Release above); CI runs the native build and
the JReleaser publish step.
3. Install β install.sh (the curl β¦ | bash one-liner above) resolves the
latest release tag, downloads the ccx binary and its checksums_sha256.txt
sidecar, verifies the SHA-256 checksum (fail-closed: aborts on a missing or
mismatched checksum), and installs the binary to ~/.local/bin/ccx. Override the
target with CCX_INSTALL_DIR; bypass verification (not recommended) with
CCX_SKIP_CHECKSUM=1.
Documented limits (current):
- Linux x86_64 only. The release builds and publishes a single
linux-x86_64native binary;install.shfetches only that binary. - No macOS build yet. macOS builds and a Homebrew tap are not configured at
all:
pom.xmldeclares a singlelinux-x86_64JReleaser artifact and no packagers. Adding them means authoring the packager config and a macOS native build job inrelease.ymlfrom scratch.