diff --git a/ROADMAP.md b/ROADMAP.md index 78385b13..33ca6114 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2072,7 +2072,7 @@ Original filing (2026-04-13): user requested a `-acp` parameter to support ACP p **Source.** Jobdori dogfood 2026-04-18 against `/tmp/cdI` on main HEAD `7f76e6b` in response to Clawhip pinpoint nudge at `1494736729582862446`. Stacks three independent failures on the permission-rule surface: (a) typo-accepting parser (truth-audit / diagnostic-integrity flavor — sibling of #86), (b) case-sensitive matcher against lowercase runtime names (reporting-surface / config-hygiene flavor — sibling of #91's alias-collapse), (c) rules invisible in every diagnostic surface (sibling of #87 permission-mode-source invisibility). Shares the permission-audit PR bundle alongside #50 / #87 / #91 — all four plug the same surface from different angles. -95. **`claw skills install ` always writes to the *user-level* registry (`~/.claw/skills/`) with no project-level scope, no uninstall subcommand, and no per-workspace confirmation — a skill installed from one workspace silently becomes active in every other workspace on the same machine** — dogfooded 2026-04-18 on main HEAD `b7539e6` from `/tmp/cdJ`. The install registry defaults to `$HOME/.claw/skills/`, the install subcommand has no sibling `uninstall` (only `/skills [list|install|help]` — no remove verb), and the installed skill is immediately visible as `active: true` under `source: user_claw` from every `claw` invocation on the same account. +95. **DONE — `claw skills install ` always writes to the *user-level* registry (`~/.claw/skills/`) with no project-level scope, no uninstall subcommand, and no per-workspace confirmation — a skill installed from one workspace silently becomes active in every other workspace on the same machine** — dogfooded 2026-04-18 on main HEAD `b7539e6` from `/tmp/cdJ`. The install registry defaults to `$HOME/.claw/skills/`, the install subcommand has no sibling `uninstall` (only `/skills [list|install|help]` — no remove verb), and the installed skill is immediately visible as `active: true` under `source: user_claw` from every `claw` invocation on the same account. **Concrete repro — cross-workspace leak.** ```sh diff --git a/rust/crates/commands/src/lib.rs b/rust/crates/commands/src/lib.rs index 62fe9df0..e1c7e4f7 100644 --- a/rust/crates/commands/src/lib.rs +++ b/rust/crates/commands/src/lib.rs @@ -2760,15 +2760,26 @@ pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R std::io::ErrorKind::InvalidInput, "missing_argument: skills install requires an install source.\nUsage: claw skills install ", )), + // #95: support --project flag for project-level install Some(args) if args.starts_with("install ") => { - let target = args["install ".len()..].trim(); + let rest = args["install ".len()..].trim(); + let (target, project_flag) = if let Some(t) = rest.strip_prefix("--project") { + (t.trim_start().trim_start_matches('=').trim(), true) + } else { + (rest, false) + }; if target.is_empty() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, - "missing_argument: skills install requires an install source.\nUsage: claw skills install ", + "missing_argument: skills install requires an install source.\nUsage: claw skills install [--project] ", )); } - let install = install_skill(target, cwd)?; + let install = if project_flag { + let project_root = cwd.join(".claw").join("skills"); + install_skill_into(target, cwd, &project_root)? + } else { + install_skill(target, cwd)? + }; Ok(render_skill_install_report(&install)) } Some("uninstall" | "remove" | "delete") => Err(std::io::Error::new( @@ -2922,16 +2933,28 @@ pub fn handle_skills_slash_command_json(args: Option<&str>, cwd: &Path) -> std:: "install_source", "Usage: claw skills install ", )), + // #95: support --project flag for project-level install Some(args) if args.starts_with("install ") => { - let target = args["install ".len()..].trim(); + let rest = args["install ".len()..].trim(); + let (target, project_flag) = if let Some(t) = rest.strip_prefix("--project") { + (t.trim_start().trim_start_matches('=').trim(), true) + } else { + (rest, false) + }; if target.is_empty() { return Ok(render_skills_missing_argument_json( "install", "install_source", - "Usage: claw skills install ", + "Usage: claw skills install [--project] ", )); } - match install_skill(target, cwd) { + let result = if project_flag { + let project_root = cwd.join(".claw").join("skills"); + install_skill_into(target, cwd, &project_root) + } else { + install_skill(target, cwd) + }; + match result { Ok(install) => Ok(render_skill_install_report_json(&install)), Err(error) => Ok(render_skill_install_error_json(target, &error)), } @@ -4888,12 +4911,12 @@ fn render_agents_usage_json(unexpected: Option<&str>) -> Value { fn render_skills_usage(unexpected: Option<&str>) -> String { let mut lines = vec![ "Skills".to_string(), - " Usage /skills [list|show |install |uninstall |help| [args]]".to_string(), + " Usage /skills [list|show |install [--project] |uninstall |help| [args]]".to_string(), " Alias /skill".to_string(), - " Direct CLI claw skills [list|show |install |uninstall |help| [args]]".to_string(), + " Direct CLI claw skills [list|show |install [--project] |uninstall |help| [args]]".to_string(), " Lifecycle install , uninstall ".to_string(), " Invoke /skills help overview -> $help overview".to_string(), - " Install root $CLAW_CONFIG_HOME/skills or ~/.claw/skills".to_string(), + " Install root $CLAW_CONFIG_HOME/skills or ~/.claw/skills (use --project for .claw/skills)".to_string(), " Sources .claw/skills, .omc/skills, .agents/skills, .codex/skills, .claude/skills, ~/.claw/skills, ~/.omc/skills, ~/.claude/skills/omc-learned, ~/.codex/skills, ~/.claude/skills, legacy /commands".to_string(), ]; if let Some(args) = unexpected {