Make .claw the only user-facing config namespace

Agents, skills, and init output were still surfacing .codex/.claude paths even though the runtime already treats .claw as the canonical config home. This updates help text, reports, skill install defaults, and repo bootstrap output to present a single .claw namespace while keeping legacy discovery fallbacks in place for existing setups.

Constraint: Existing .codex/.claude agent and skill directories still need to load for compatibility
Rejected: Remove legacy discovery entirely | would break existing user setups instead of just cleaning up surfaced output
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep future user-facing config, agent, and skill path copy aligned to .claw and  even when legacy fallbacks remain supported internally
Tested: cargo fmt --all --check; cargo test --workspace --exclude compat-harness
Not-tested: cargo clippy --workspace --all-targets -- -D warnings | fails in pre-existing unrelated runtime files (for example mcp_lifecycle_hardened.rs, mcp_tool_bridge.rs, lsp_client.rs, permission_enforcer.rs, recovery_recipes.rs, stale_branch.rs, task_registry.rs, team_cron_registry.rs, worker_boot.rs)
This commit is contained in:
Yeachan-Heo
2026-04-05 17:27:46 +00:00
parent 9b156e21cf
commit 2dd05bfcef
3 changed files with 148 additions and 59 deletions

View File

@@ -1954,25 +1954,49 @@ pub struct PluginsCommandResult {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum DefinitionSource {
ProjectClaw,
ProjectCodex,
ProjectClaude,
UserClawConfigHome,
UserCodexHome,
UserClaw,
UserCodex,
UserClaude,
}
impl DefinitionSource {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum DefinitionScope {
Project,
UserConfigHome,
UserHome,
}
impl DefinitionScope {
fn label(self) -> &'static str {
match self {
Self::ProjectCodex => "Project (.codex)",
Self::ProjectClaude => "Project (.claude)",
Self::UserCodexHome => "User ($CODEX_HOME)",
Self::UserCodex => "User (~/.codex)",
Self::UserClaude => "User (~/.claude)",
Self::Project => "Project (.claw)",
Self::UserConfigHome => "User ($CLAW_CONFIG_HOME)",
Self::UserHome => "User (~/.claw)",
}
}
}
impl DefinitionSource {
fn report_scope(self) -> DefinitionScope {
match self {
Self::ProjectClaw | Self::ProjectCodex | Self::ProjectClaude => {
DefinitionScope::Project
}
Self::UserClawConfigHome | Self::UserCodexHome => DefinitionScope::UserConfigHome,
Self::UserClaw | Self::UserCodex | Self::UserClaude => DefinitionScope::UserHome,
}
}
fn label(self) -> &'static str {
self.report_scope().label()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct AgentSummary {
name: String,
@@ -2302,6 +2326,11 @@ fn discover_definition_roots(cwd: &Path, leaf: &str) -> Vec<(DefinitionSource, P
let mut roots = Vec::new();
for ancestor in cwd.ancestors() {
push_unique_root(
&mut roots,
DefinitionSource::ProjectClaw,
ancestor.join(".claw").join(leaf),
);
push_unique_root(
&mut roots,
DefinitionSource::ProjectCodex,
@@ -2314,6 +2343,14 @@ fn discover_definition_roots(cwd: &Path, leaf: &str) -> Vec<(DefinitionSource, P
);
}
if let Ok(claw_config_home) = env::var("CLAW_CONFIG_HOME") {
push_unique_root(
&mut roots,
DefinitionSource::UserClawConfigHome,
PathBuf::from(claw_config_home).join(leaf),
);
}
if let Ok(codex_home) = env::var("CODEX_HOME") {
push_unique_root(
&mut roots,
@@ -2324,6 +2361,11 @@ fn discover_definition_roots(cwd: &Path, leaf: &str) -> Vec<(DefinitionSource, P
if let Some(home) = env::var_os("HOME") {
let home = PathBuf::from(home);
push_unique_root(
&mut roots,
DefinitionSource::UserClaw,
home.join(".claw").join(leaf),
);
push_unique_root(
&mut roots,
DefinitionSource::UserCodex,
@@ -2343,6 +2385,12 @@ fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
let mut roots = Vec::new();
for ancestor in cwd.ancestors() {
push_unique_skill_root(
&mut roots,
DefinitionSource::ProjectClaw,
ancestor.join(".claw").join("skills"),
SkillOrigin::SkillsDir,
);
push_unique_skill_root(
&mut roots,
DefinitionSource::ProjectCodex,
@@ -2355,6 +2403,12 @@ fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
ancestor.join(".claude").join("skills"),
SkillOrigin::SkillsDir,
);
push_unique_skill_root(
&mut roots,
DefinitionSource::ProjectClaw,
ancestor.join(".claw").join("commands"),
SkillOrigin::LegacyCommandsDir,
);
push_unique_skill_root(
&mut roots,
DefinitionSource::ProjectCodex,
@@ -2369,6 +2423,22 @@ fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
);
}
if let Ok(claw_config_home) = env::var("CLAW_CONFIG_HOME") {
let claw_config_home = PathBuf::from(claw_config_home);
push_unique_skill_root(
&mut roots,
DefinitionSource::UserClawConfigHome,
claw_config_home.join("skills"),
SkillOrigin::SkillsDir,
);
push_unique_skill_root(
&mut roots,
DefinitionSource::UserClawConfigHome,
claw_config_home.join("commands"),
SkillOrigin::LegacyCommandsDir,
);
}
if let Ok(codex_home) = env::var("CODEX_HOME") {
let codex_home = PathBuf::from(codex_home);
push_unique_skill_root(
@@ -2387,6 +2457,18 @@ fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
if let Some(home) = env::var_os("HOME") {
let home = PathBuf::from(home);
push_unique_skill_root(
&mut roots,
DefinitionSource::UserClaw,
home.join(".claw").join("skills"),
SkillOrigin::SkillsDir,
);
push_unique_skill_root(
&mut roots,
DefinitionSource::UserClaw,
home.join(".claw").join("commands"),
SkillOrigin::LegacyCommandsDir,
);
push_unique_skill_root(
&mut roots,
DefinitionSource::UserCodex,
@@ -2467,15 +2549,18 @@ fn install_skill_into(
}
fn default_skill_install_root() -> std::io::Result<PathBuf> {
if let Ok(claw_config_home) = env::var("CLAW_CONFIG_HOME") {
return Ok(PathBuf::from(claw_config_home).join("skills"));
}
if let Ok(codex_home) = env::var("CODEX_HOME") {
return Ok(PathBuf::from(codex_home).join("skills"));
}
if let Some(home) = env::var_os("HOME") {
return Ok(PathBuf::from(home).join(".codex").join("skills"));
return Ok(PathBuf::from(home).join(".claw").join("skills"));
}
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"unable to resolve a skills install root; set CODEX_HOME or HOME",
"unable to resolve a skills install root; set CLAW_CONFIG_HOME or HOME",
))
}
@@ -2841,22 +2926,20 @@ fn render_agents_report(agents: &[AgentSummary]) -> String {
String::new(),
];
for source in [
DefinitionSource::ProjectCodex,
DefinitionSource::ProjectClaude,
DefinitionSource::UserCodexHome,
DefinitionSource::UserCodex,
DefinitionSource::UserClaude,
for scope in [
DefinitionScope::Project,
DefinitionScope::UserConfigHome,
DefinitionScope::UserHome,
] {
let group = agents
.iter()
.filter(|agent| agent.source == source)
.filter(|agent| agent.source.report_scope() == scope)
.collect::<Vec<_>>();
if group.is_empty() {
continue;
}
lines.push(format!("{}:", source.label()));
lines.push(format!("{}:", scope.label()));
for agent in group {
let detail = agent_detail(agent);
match agent.shadowed_by {
@@ -2899,22 +2982,20 @@ fn render_skills_report(skills: &[SkillSummary]) -> String {
String::new(),
];
for source in [
DefinitionSource::ProjectCodex,
DefinitionSource::ProjectClaude,
DefinitionSource::UserCodexHome,
DefinitionSource::UserCodex,
DefinitionSource::UserClaude,
for scope in [
DefinitionScope::Project,
DefinitionScope::UserConfigHome,
DefinitionScope::UserHome,
] {
let group = skills
.iter()
.filter(|skill| skill.source == source)
.filter(|skill| skill.source.report_scope() == scope)
.collect::<Vec<_>>();
if group.is_empty() {
continue;
}
lines.push(format!("{}:", source.label()));
lines.push(format!("{}:", scope.label()));
for skill in group {
let mut parts = vec![skill.name.clone()];
if let Some(description) = &skill.description {
@@ -3080,7 +3161,7 @@ fn render_agents_usage(unexpected: Option<&str>) -> String {
"Agents".to_string(),
" Usage /agents [list|help]".to_string(),
" Direct CLI claw agents".to_string(),
" Sources .codex/agents, .claude/agents, $CODEX_HOME/agents".to_string(),
" Sources .claw/agents, ~/.claw/agents, $CLAW_CONFIG_HOME/agents".to_string(),
];
if let Some(args) = unexpected {
lines.push(format!(" Unexpected {args}"));
@@ -3093,8 +3174,8 @@ fn render_skills_usage(unexpected: Option<&str>) -> String {
"Skills".to_string(),
" Usage /skills [list|install <path>|help]".to_string(),
" Direct CLI claw skills [list|install <path>|help]".to_string(),
" Install root $CODEX_HOME/skills or ~/.codex/skills".to_string(),
" Sources .codex/skills, .claude/skills, legacy /commands".to_string(),
" Install root $CLAW_CONFIG_HOME/skills or ~/.claw/skills".to_string(),
" Sources .claw/skills, ~/.claw/skills, legacy /commands".to_string(),
];
if let Some(args) = unexpected {
lines.push(format!(" Unexpected {args}"));
@@ -3933,7 +4014,7 @@ mod tests {
let workspace = temp_dir("agents-workspace");
let project_agents = workspace.join(".codex").join("agents");
let user_home = temp_dir("agents-home");
let user_agents = user_home.join(".codex").join("agents");
let user_agents = user_home.join(".claude").join("agents");
write_agent(
&project_agents,
@@ -3966,10 +4047,10 @@ mod tests {
assert!(report.contains("Agents"));
assert!(report.contains("2 active agents"));
assert!(report.contains("Project (.codex):"));
assert!(report.contains("Project (.claw):"));
assert!(report.contains("planner · Project planner · gpt-5.4 · medium"));
assert!(report.contains("User (~/.codex):"));
assert!(report.contains("(shadowed by Project (.codex)) planner · User planner"));
assert!(report.contains("User (~/.claw):"));
assert!(report.contains("(shadowed by Project (.claw)) planner · User planner"));
assert!(report.contains("verifier · Verification agent · gpt-5.4-mini · high"));
let _ = fs::remove_dir_all(workspace);
@@ -4011,12 +4092,11 @@ mod tests {
assert!(report.contains("Skills"));
assert!(report.contains("3 available skills"));
assert!(report.contains("Project (.codex):"));
assert!(report.contains("Project (.claw):"));
assert!(report.contains("plan · Project planning guidance"));
assert!(report.contains("Project (.claude):"));
assert!(report.contains("deploy · Legacy deployment guidance · legacy /commands"));
assert!(report.contains("User (~/.codex):"));
assert!(report.contains("(shadowed by Project (.codex)) plan · User planning guidance"));
assert!(report.contains("User (~/.claw):"));
assert!(report.contains("(shadowed by Project (.claw)) plan · User planning guidance"));
assert!(report.contains("help · Help guidance"));
let _ = fs::remove_dir_all(workspace);
@@ -4031,6 +4111,8 @@ mod tests {
super::handle_agents_slash_command(Some("help"), &cwd).expect("agents help");
assert!(agents_help.contains("Usage /agents [list|help]"));
assert!(agents_help.contains("Direct CLI claw agents"));
assert!(agents_help
.contains("Sources .claw/agents, ~/.claw/agents, $CLAW_CONFIG_HOME/agents"));
let agents_unexpected =
super::handle_agents_slash_command(Some("show planner"), &cwd).expect("agents usage");
@@ -4039,7 +4121,7 @@ mod tests {
let skills_help =
super::handle_skills_slash_command(Some("--help"), &cwd).expect("skills help");
assert!(skills_help.contains("Usage /skills [list|install <path>|help]"));
assert!(skills_help.contains("Install root $CODEX_HOME/skills or ~/.codex/skills"));
assert!(skills_help.contains("Install root $CLAW_CONFIG_HOME/skills or ~/.claw/skills"));
assert!(skills_help.contains("legacy /commands"));
let skills_unexpected =
@@ -4213,7 +4295,7 @@ mod tests {
let listed = render_skills_report(
&load_skills_from_roots(&roots).expect("installed skills should load"),
);
assert!(listed.contains("User ($CODEX_HOME):"));
assert!(listed.contains("User ($CLAW_CONFIG_HOME):"));
assert!(listed.contains("help · Helpful skill"));
let _ = fs::remove_dir_all(workspace);

View File

@@ -1,7 +1,7 @@
use std::fs;
use std::path::{Path, PathBuf};
const STARTER_CLAUDE_JSON: &str = concat!(
const STARTER_CLAW_JSON: &str = concat!(
"{\n",
" \"permissions\": {\n",
" \"defaultMode\": \"dontAsk\"\n",
@@ -9,7 +9,7 @@ const STARTER_CLAUDE_JSON: &str = concat!(
"}\n",
);
const GITIGNORE_COMMENT: &str = "# Claw Code local artifacts";
const GITIGNORE_ENTRIES: [&str; 2] = [".claude/settings.local.json", ".claude/sessions/"];
const GITIGNORE_ENTRIES: [&str; 2] = [".claw/settings.local.json", ".claw/sessions/"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum InitStatus {
@@ -80,16 +80,16 @@ struct RepoDetection {
pub(crate) fn initialize_repo(cwd: &Path) -> Result<InitReport, Box<dyn std::error::Error>> {
let mut artifacts = Vec::new();
let claude_dir = cwd.join(".claude");
let claw_dir = cwd.join(".claw");
artifacts.push(InitArtifact {
name: ".claude/",
status: ensure_dir(&claude_dir)?,
name: ".claw/",
status: ensure_dir(&claw_dir)?,
});
let claude_json = cwd.join(".claude.json");
let claw_json = cwd.join(".claw.json");
artifacts.push(InitArtifact {
name: ".claude.json",
status: write_file_if_missing(&claude_json, STARTER_CLAUDE_JSON)?,
name: ".claw.json",
status: write_file_if_missing(&claw_json, STARTER_CLAW_JSON)?,
});
let gitignore = cwd.join(".gitignore");
@@ -209,7 +209,7 @@ pub(crate) fn render_init_claude_md(cwd: &Path) -> String {
lines.push("## Working agreement".to_string());
lines.push("- Prefer small, reviewable changes and keep generated bootstrap files aligned with actual repo workflows.".to_string());
lines.push("- Keep shared defaults in `.claude.json`; reserve `.claude/settings.local.json` for machine-local overrides.".to_string());
lines.push("- Keep shared defaults in `.claw.json`; reserve `.claw/settings.local.json` for machine-local overrides.".to_string());
lines.push("- Do not overwrite existing `CLAUDE.md` content automatically; update it intentionally when repo workflows change.".to_string());
lines.push(String::new());
@@ -354,15 +354,16 @@ mod tests {
let report = initialize_repo(&root).expect("init should succeed");
let rendered = report.render();
assert!(rendered.contains(".claude/ created"));
assert!(rendered.contains(".claude.json created"));
assert!(rendered.contains(".claw/"));
assert!(rendered.contains(".claw.json"));
assert!(rendered.contains("created"));
assert!(rendered.contains(".gitignore created"));
assert!(rendered.contains("CLAUDE.md created"));
assert!(root.join(".claude").is_dir());
assert!(root.join(".claude.json").is_file());
assert!(root.join(".claw").is_dir());
assert!(root.join(".claw.json").is_file());
assert!(root.join("CLAUDE.md").is_file());
assert_eq!(
fs::read_to_string(root.join(".claude.json")).expect("read claude json"),
fs::read_to_string(root.join(".claw.json")).expect("read claw json"),
concat!(
"{\n",
" \"permissions\": {\n",
@@ -372,8 +373,8 @@ mod tests {
)
);
let gitignore = fs::read_to_string(root.join(".gitignore")).expect("read gitignore");
assert!(gitignore.contains(".claude/settings.local.json"));
assert!(gitignore.contains(".claude/sessions/"));
assert!(gitignore.contains(".claw/settings.local.json"));
assert!(gitignore.contains(".claw/sessions/"));
let claude_md = fs::read_to_string(root.join("CLAUDE.md")).expect("read claude md");
assert!(claude_md.contains("Languages: Rust."));
assert!(claude_md.contains("cargo clippy --workspace --all-targets -- -D warnings"));
@@ -386,8 +387,7 @@ mod tests {
let root = temp_dir();
fs::create_dir_all(&root).expect("create root");
fs::write(root.join("CLAUDE.md"), "custom guidance\n").expect("write existing claude md");
fs::write(root.join(".gitignore"), ".claude/settings.local.json\n")
.expect("write gitignore");
fs::write(root.join(".gitignore"), ".claw/settings.local.json\n").expect("write gitignore");
let first = initialize_repo(&root).expect("first init should succeed");
assert!(first
@@ -395,8 +395,9 @@ mod tests {
.contains("CLAUDE.md skipped (already exists)"));
let second = initialize_repo(&root).expect("second init should succeed");
let second_rendered = second.render();
assert!(second_rendered.contains(".claude/ skipped (already exists)"));
assert!(second_rendered.contains(".claude.json skipped (already exists)"));
assert!(second_rendered.contains(".claw/"));
assert!(second_rendered.contains(".claw.json"));
assert!(second_rendered.contains("skipped (already exists)"));
assert!(second_rendered.contains(".gitignore skipped (already exists)"));
assert!(second_rendered.contains("CLAUDE.md skipped (already exists)"));
assert_eq!(
@@ -404,8 +405,8 @@ mod tests {
"custom guidance\n"
);
let gitignore = fs::read_to_string(root.join(".gitignore")).expect("read gitignore");
assert_eq!(gitignore.matches(".claude/settings.local.json").count(), 1);
assert_eq!(gitignore.matches(".claude/sessions/").count(), 1);
assert_eq!(gitignore.matches(".claw/settings.local.json").count(), 1);
assert_eq!(gitignore.matches(".claw/sessions/").count(), 1);
fs::remove_dir_all(root).expect("cleanup temp dir");
}

View File

@@ -2976,15 +2976,21 @@ fn resolve_skill_path(skill: &str) -> Result<std::path::PathBuf, String> {
}
let mut candidates = Vec::new();
if let Ok(claw_config_home) = std::env::var("CLAW_CONFIG_HOME") {
candidates.push(std::path::PathBuf::from(claw_config_home).join("skills"));
}
if let Ok(codex_home) = std::env::var("CODEX_HOME") {
candidates.push(std::path::PathBuf::from(codex_home).join("skills"));
}
if let Ok(home) = std::env::var("HOME") {
let home = std::path::PathBuf::from(home);
candidates.push(home.join(".claw").join("skills"));
candidates.push(home.join(".agents").join("skills"));
candidates.push(home.join(".config").join("opencode").join("skills"));
candidates.push(home.join(".codex").join("skills"));
candidates.push(home.join(".claude").join("skills"));
}
candidates.push(std::path::PathBuf::from("/home/bellman/.claw/skills"));
candidates.push(std::path::PathBuf::from("/home/bellman/.codex/skills"));
for root in candidates {