ccx is a CLI tool for managing fine-grained Claude Code configuration per project. It handles plugin enablement, CLAUDE.md fragment composition, MCP server management, skill overrides, rule files, and profile lifecycle — eliminating the need to manually edit .claude/settings.json, CLAUDE.md, or related files. A full-screen TUI dashboard provides an interactive view of all configuration dimensions. Profiles define reusable configuration bundles that can be enabled, disabled, and drift-detected.
Picocli @Command classes: one per top-level command. Each top-level command is a CDI @Dependent bean (plan 036 Wave 1 flipped them from @ApplicationScoped — the ARC client proxy nulled picocli's @Spec field injection in production). Subcommands are static nested classes that use field injection (explicit CDI exception). Commands are the only entry point for user input; they delegate all business logic to services.
CDI @ApplicationScoped beans containing all business logic. Services are the only classes that read and write files. Each service follows the atomic-write invariant: all file writes go through a .tmp sibling + Files.move(REPLACE_EXISTING, ATOMIC_MOVE).
Key services:
SettingsService— reads/writes.claude/settings.jsonProfileService— loads and resolves profile YAML from the on-disk ccx config dir; supports multi-level inheritanceProfileApplyService— applies a resolved profile (plugins, fragments, rules, permissions) to a project; also removes profile resourcesCcxContextService— manages.ccx/context.jsonlock file tracking which profiles are enabled and their installation snapshot; provides drift detectionClaudeMdService— reads/writes the<!-- ccx:begin -->…<!-- ccx:end -->marker block inCLAUDE.mdPluginMapService— discovers and maps plugin short names to qualified keys from marketplace directoriesSkillsService— discovers skills from configured marketplace pathsMarketplaceCloneService— clones/pulls marketplace plugin repositories for offline discovery
Jackson-annotated records/POJOs for serialization. No business logic. Key types: Settings, Profile, CcxContext, CcxContext.ProfileSnapshot, Permissions, McpServer.
Settings uses an inner Builder class for deserialization (@JsonDeserialize(builder=Settings.Builder.class) + @JsonPOJOBuilder(withPrefix="")) to support @JsonAnySetter passthrough of unknown fields — records cannot carry @JsonAnySetter directly. Both Settings and Settings.Builder are annotated @RegisterForReflection (inner classes require a separate annotation for GraalVM native).
Marketplace cluster: MarketplaceManifest, MarketplacePlugin, MarketplaceSource
(with custom MarketplaceSourceSerializer/Deserializer), PluginMapCache.
Profile sub-records: ProfileMetadata, ProfilePlugins, ProfileClaudeMd.
Context sub-record: CcxContext.ProfileRecord.
Tamboui-based interactive terminal dashboard launched by TuiCommand. Architecture:
TuiDataService(CDI) — buildsTuiSnapshot, the full immutable data graph for all tab rendersTuiSnapshot— value object containing all data needed to render every tab; built once per actionCcxApp— top-level Tamboui application; ownsTuiStateandTuiActions; dispatches key eventsTuiState— mutable state: active tab, selected rows, filter focus, snapshot referenceTuiActions— non-CDI plain Java object; the only TUI class that mutates project state; delegates to servicesFilterState— per-tab filter state wrapping TambouiTextInputState- Tab classes (
tabs/) — pure renderers implementingFilterableTab<T>; no service injection; no state mutation
NativeConfig registers classes that require reflection at build time via @RegisterForReflection. Also declares initialize-at-run-time entries for JLine/JNA native backend classes that cannot be initialized at image build time.
Stateless static helpers: FileUtils.atomicWrite (write-to-.tmp then Files.move ATOMIC_MOVE)
and ListUtils.orderedUnion (null-safe ordered dedup of two lists).
CDI producer class ObjectMapperConfig: produces @Named("jsonMapper") (Jackson JSON) and
@Named("yamlMapper") (Jackson YAML) ObjectMapper beans, both configured with
ACCEPT_CASE_INSENSITIVE_PROPERTIES, JavaTimeModule, and no-timestamps serialization.
cli/
ProfilesCommand → ProfileApplyService, CcxContextService, ProfileService, CcxConfigService, SettingsService
PluginsCommand → SettingsService, PluginMapService, MarketplaceCloneService
SkillsCommand → SettingsService, SkillsService
McpCommand → SettingsService
ClaudeMdCommand → ClaudeMdService, CcxConfigService
DoctorCommand → SettingsService, PluginMapService, ClaudeMdService, CcxConfigService, ProfileService, CcxContextService
StatusCommand → SettingsService, ClaudeMdService
CleanCommand → SettingsService, ClaudeMdService
ConfigCommand → CcxConfigService
TuiCommand → TuiDataService, ProfileApplyService, CcxContextService, ProfileService,
SettingsService, ClaudeMdService, MarketplaceCloneService, PluginMapService
(`init` and `compose` top-level commands were removed in plan 036 Wave 2; `resolveDependencyClosure` and `.claudeignore` bootstrap migrated to ProfileService / ProfileApplyService.)
service/
ProfileApplyService → SettingsService, ClaudeMdService, PluginMapService, CcxConfigService
CcxContextService → ClaudeMdService (for readMarkerBlock, fragment drift check)
ProfileService → CcxConfigService (loads YAML from config dir)
SettingsService (no service deps — Jackson + filesystem)
ClaudeMdService (no service deps — filesystem only)
SkillsService → SettingsService
PluginMapService → MarketplaceCloneService (Jackson + filesystem)
tui/
TuiDataService → SettingsService, ClaudeMdService, CcxContextService,
ProfileService, PluginMapService, CcxConfigService
TuiActions → SettingsService, ClaudeMdService, TuiDataService,
ProfileApplyService, CcxContextService, ProfileService
CcxApp (plain Java; constructed by TuiCommand with injected services)
tabs/FilterableTab (abstract; implemented by 4 list-tab classes — no service deps)
(OverviewTab is a standalone final class; it does not extend FilterableTab)
Dependency direction: cli → service → model. The tui package depends on service and model but CLI commands do not import TUI classes (except TuiCommand which instantiates CcxApp).
- Picocli parses args → routes to
ProfilesCommand.EnableCommand.call() ProfileService.load("java")→ reads~/.config/ccx/profiles/java/profile.yamlfrom diskProfileService.resolve(profile)→ merges multi-level inheritance chainProfileApplyService.apply(resolved, dir, false):- Reads existing
settings.jsonviaSettingsService.read() - Adds plugin keys to
enabledPlugins - Merges permissions
- Writes updated
settings.jsonatomically - Writes rule files to
.claude/rules/atomically - Writes fragment lines to
CLAUDE.mdmarker block atomically - Returns
ProfileSnapshot(what was written)
- Reads existing
CcxContextService.recordEnabled("java", snapshot, dir):- Reads
.ccx/context.json - Adds the profile record with timestamp + snapshot
- Writes
.ccx/context.jsonatomically
- Reads
- list/confirmation commands print to stdout via the picocli
PrintWriter+out.flush()(theInlineRendererinteractive path was removed in plan 036 Wave 3)
CcxApp.handleEvent()→ dispatches tohandleProfileAction("enable")TuiActions.enableProfile(name, projectDir)→ calls services (same as above)- Returns new
TuiSnapshotfromTuiDataService.snapshot(projectDir) TuiState.setSnapshot(newSnap)— replaces immutable snapshot- Tamboui calls
CcxApp.renderFrame()on next tick → all tabs re-render from new snapshot (pure)
- CLI layer delegates business logic to services — edge-case commands (
CleanCommand,ProfilesCommand.NewCommand,ConfigCommand) do direct atomic writes/deletes where no service boundary is warranted; all other I/O goes through services - Tab classes are pure renderers — no service injection, no file I/O, no state mutation
TuiActionsis the only TUI class that mutates project state — all other TUI classes are read-only- All file writes are atomic — write to
.tmpsibling, thenFiles.move(REPLACE_EXISTING, ATOMIC_MOVE) settings.jsonis the source of truth for plugins, permissions, MCP servers, and skill overrides.ccx/context.jsontracks profile membership — NOT.claude/(that belongs to Claude Code)- Profiles live on disk under
~/.config/ccx/profiles/<name>/profile.yaml— there are no bundled classpath profiles (src/main/resources/profiles/is empty); users author their own - Path boundary checks — all user-supplied paths (e.g.
--project-dir,--output, marketplace paths) are validated to stay withinuser.home,user.dir, orjava.io.tmpdir
me.neveux.ccx.service.PathSecurity is a final utility class with two static methods:
| Method | Semantics |
|---|---|
isAllowed(Path) |
Lenient — allows non-existent paths (for future writes). On IOException from toRealPath(), falls back to lexical toAbsolutePath().normalize(). Null → false. |
resolveAndCheck(Path) |
Strict — non-existent paths return false (can't verify, reject). On IOException → false. Used for read/exec operations where the path must already exist. |
Allowed boundaries (checked against resolved path):
System.getProperty("user.home")— the user's home directory treeSystem.getProperty("user.dir")— the current working directory treeSystem.getProperty("java.io.tmpdir")— the system temp directory tree
Symlink handling: both methods call Path.toRealPath() first, which resolves all symlinks to their final target. This prevents a symlink like /tmp/evil → /etc from bypassing the boundary check — the real path /etc does not start with any allowed boundary.
Command helpers in CommandUtil wrap PathSecurity.isAllowed():
CommandUtil.checkPathSecurity(dir, spec)— prints a standard error message and returns false on rejection; commands return exit code 1 immediately.