Skip to content

Commit 7d63699

Browse files
committed
feat: ultraworkers#145 — wire claw plugins subcommand to CLI parser (prompt misdelivery fix)
## Problem `claw plugins` (and `claw plugins list`, `claw plugins --help`, `claw plugins info <name>`, etc.) fell through the top-level subcommand match and got routed into the prompt-execution path. Result: a purely local introspection command triggered an Anthropic API call and surfaced `missing Anthropic credentials` to the user. With valid credentials, it would actually send the literal string "plugins" as a user prompt to Claude, burning tokens for a local query. $ claw plugins error: missing Anthropic credentials; export ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY before calling the Anthropic API $ ANTHROPIC_API_KEY=dummy claw plugins ⠋ 🦀 Thinking... ✘ ❌ Request failed error: api returned 401 Unauthorized Meanwhile siblings (`agents`, `mcp`, `skills`) all worked correctly: $ claw agents No agents found. $ claw mcp MCP Working directory ... Configured servers 0 ## Root cause `CliAction::Plugins` exists, has a working dispatcher (`LiveCli::print_plugins`), and is produced inside the REPL via `SlashCommand::Plugins`. But the top-level CLI parser in `parse_subcommand()` had arms for `agents`, `mcp`, `skills`, `status`, `doctor`, `init`, `export`, `prompt`, etc., and **no arm for `plugins`**. The dispatch never ran from the CLI entry point. ## Changes ### rust/crates/rusty-claude-cli/src/main.rs Added a `"plugins"` arm to the top-level match in `parse_subcommand()` that produces `CliAction::Plugins { action, target, output_format }`, following the same positional convention as `mcp` (`action` = first positional, `target` = second). Rejects >2 positional args with a clear error. Added four regression assertions in the existing `parse_args` test: - `plugins` alone → `CliAction::Plugins { action: None, target: None }` - `plugins list` → action: Some("list"), target: None - `plugins enable <name>` → action: Some("enable"), target: Some(...) - `plugins --output-format json` → action: None, output_format: Json ### ROADMAP.md Added Pinpoint ultraworkers#145 documenting the gap, verification, root cause, fix shape, and acceptance. ## Live verification $ claw plugins # no credentials set Plugins example-bundled v0.1.0 disabled sample-hooks v0.1.0 disabled $ claw plugins --output-format json # no credentials set { "action": "list", "kind": "plugin", "message": "Plugins\n example-bundled ...\n sample-hooks ...", "reload_runtime": false, "target": null } Exit 0 in all modes. No network call. No "missing credentials" error. ## Tests - rusty-claude-cli bin: 177 tests pass (new plugin assertions included) - Full workspace green except pre-existing resume_latest flake (unrelated) Closes ROADMAP ultraworkers#145.
1 parent faeaa1d commit 7d63699

2 files changed

Lines changed: 128 additions & 0 deletions

File tree

ROADMAP.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5520,3 +5520,61 @@ Exit 0. Full envelope with error surfaced.
55205520
**Future phase (joins #143 Phase 2).** When typed-error taxonomy lands (§4.44), promote `config_load_error` from string to typed object across `doctor`, `status`, and `mcp` in one pass.
55215521

55225522
**Source.** Jobdori dogfood 2026-04-21 18:59 KST on main HEAD `e2a43fc`. Joins **partial-success** cluster (#143, Principle #5) and **surface consistency** cluster. Session tally: ROADMAP #144.
5523+
5524+
## Pinpoint #145. `claw plugins` subcommand not wired to CLI parser — word gets treated as a prompt, hits Anthropic API
5525+
5526+
**Gap.** `claw plugins` (and `claw plugins list`, `claw plugins --help`, `claw plugins info <name>`, etc.) fall through the top-level subcommand match and get routed into the prompt-execution path. Result: a purely local introspection command triggers an Anthropic API call and surfaces `missing Anthropic credentials` to the user. With valid credentials, it would actually send the string `"plugins"` as a prompt to Claude, burning tokens for a local query.
5527+
5528+
**Verified on main HEAD `faeaa1d` (2026-04-21 19:32 KST):**
5529+
5530+
```
5531+
$ claw plugins
5532+
error: missing Anthropic credentials; export ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY before calling the Anthropic API
5533+
5534+
$ claw plugins --output-format json
5535+
{"error":"missing Anthropic credentials; ...","type":"error"}
5536+
5537+
$ claw plugins --help
5538+
error: missing Anthropic credentials; ...
5539+
5540+
$ claw plugins list
5541+
error: missing Anthropic credentials; ...
5542+
5543+
$ ANTHROPIC_API_KEY=dummy claw plugins
5544+
⠋ 🦀 Thinking...
5545+
✘ ❌ Request failed
5546+
error: api returned 401 Unauthorized (authentication_error)
5547+
```
5548+
5549+
Compare `agents`, `mcp`, `skills` — all recognized, all local, all exit 0:
5550+
5551+
```
5552+
$ claw agents
5553+
No agents found.
5554+
$ claw mcp
5555+
MCP
5556+
Working directory ...
5557+
Configured servers 0
5558+
```
5559+
5560+
**Root cause.** In `rusty-claude-cli/src/main.rs`, the top-level `match rest[0].as_str()` parser has arms for `agents`, `mcp`, `skills`, `status`, `doctor`, `init`, `export`, `prompt`, etc., but **no arm for `plugins`**. The `CliAction::Plugins` variant exists, has a dispatcher (`print_plugins`), and is produced by `SlashCommand::Plugins` inside the REPL — but the top-level CLI path was never wired. Result: `plugins` matches neither a known subcommand nor a slash path, so it falls through to the default "run as prompt" behavior.
5561+
5562+
**Why this is a clawability gap.**
5563+
1. **Prompt misdelivery (explicit Clawhip category)**: the command string is sent to the LLM instead of dispatched locally. Real risk: without the credentials guard, `claw plugins` would send `"plugins"` as a user prompt to Claude, burning tokens.
5564+
2. **Surface asymmetry**: `plugins` is the only diagnostic-adjacent command that isn't wired. Documentation, slash command, and dispatcher all exist; parser wiring was missed.
5565+
3. **`--help` should never hit the network**. Anywhere.
5566+
4. **Misleading error**: user running `claw plugins` sees an Anthropic credential error. No hint that `plugins` wasn't a recognized subcommand.
5567+
5568+
**Fix shape (~20 lines).** Add a `"plugins"` arm to the top-level parser in `main.rs` that produces `CliAction::Plugins { action, target, output_format }`, following the same positional convention as `mcp` (`action` = first positional, `target` = second). The existing `CliAction::Plugins` handler (`LiveCli::print_plugins`) already covers text and JSON.
5569+
5570+
**Acceptance.**
5571+
- `claw plugins` exits 0 with plugins list (empty in a clean workspace, which is the honest state).
5572+
- `claw plugins --output-format json` emits `{"kind":"plugin","action":"list",...}` with exit 0.
5573+
- `claw plugins list` exits 0 and matches `claw plugins`.
5574+
- `claw plugins info <name>` resolves through the existing handler.
5575+
- No Anthropic network call occurs for any `plugins` invocation.
5576+
- Regression test: parse `["claw", "plugins"]`, assert `CliAction::Plugins { action: None, target: None, .. }`.
5577+
5578+
**Blocker.** None. `CliAction::Plugins` already exists with a working dispatcher.
5579+
5580+
**Source.** Jobdori dogfood 2026-04-21 19:30 KST on main HEAD `faeaa1d` in response to Clawhip nudge. Joins **prompt misdelivery** cluster. Session tally: ROADMAP #145.

rust/crates/rusty-claude-cli/src/main.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,30 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
661661
args: join_optional_args(&rest[1..]),
662662
output_format,
663663
}),
664+
// #145: `plugins` was routed through the prompt fallback because no
665+
// top-level parser arm produced CliAction::Plugins. That made `claw
666+
// plugins` (and `claw plugins --help`, `claw plugins list`, ...)
667+
// attempt an Anthropic network call, surfacing the misleading error
668+
// `missing Anthropic credentials` even though the command is purely
669+
// local introspection. Mirror `agents`/`mcp`/`skills`: action is the
670+
// first positional arg, target is the second.
671+
"plugins" => {
672+
let tail = &rest[1..];
673+
let action = tail.first().cloned();
674+
let target = tail.get(1).cloned();
675+
if tail.len() > 2 {
676+
return Err(format!(
677+
"unexpected extra arguments after `claw plugins {}`: {}",
678+
tail[..2].join(" "),
679+
tail[2..].join(" ")
680+
));
681+
}
682+
Ok(CliAction::Plugins {
683+
action,
684+
target,
685+
output_format,
686+
})
687+
}
664688
"skills" => {
665689
let args = join_optional_args(&rest[1..]);
666690
match classify_skills_slash_command(args.as_deref()) {
@@ -9549,6 +9573,52 @@ mod tests {
95499573
output_format: CliOutputFormat::Text,
95509574
}
95519575
);
9576+
// #145: `plugins` must parse as CliAction::Plugins (not fall through
9577+
// to the prompt path, which would hit the Anthropic API for a purely
9578+
// local introspection command).
9579+
assert_eq!(
9580+
parse_args(&["plugins".to_string()]).expect("plugins should parse"),
9581+
CliAction::Plugins {
9582+
action: None,
9583+
target: None,
9584+
output_format: CliOutputFormat::Text,
9585+
}
9586+
);
9587+
assert_eq!(
9588+
parse_args(&["plugins".to_string(), "list".to_string()])
9589+
.expect("plugins list should parse"),
9590+
CliAction::Plugins {
9591+
action: Some("list".to_string()),
9592+
target: None,
9593+
output_format: CliOutputFormat::Text,
9594+
}
9595+
);
9596+
assert_eq!(
9597+
parse_args(&[
9598+
"plugins".to_string(),
9599+
"enable".to_string(),
9600+
"example-bundled".to_string(),
9601+
])
9602+
.expect("plugins enable <target> should parse"),
9603+
CliAction::Plugins {
9604+
action: Some("enable".to_string()),
9605+
target: Some("example-bundled".to_string()),
9606+
output_format: CliOutputFormat::Text,
9607+
}
9608+
);
9609+
assert_eq!(
9610+
parse_args(&[
9611+
"plugins".to_string(),
9612+
"--output-format".to_string(),
9613+
"json".to_string(),
9614+
])
9615+
.expect("plugins --output-format json should parse"),
9616+
CliAction::Plugins {
9617+
action: None,
9618+
target: None,
9619+
output_format: CliOutputFormat::Json,
9620+
}
9621+
);
95529622
}
95539623

95549624
#[test]

0 commit comments

Comments
 (0)