Repository navigation
ScriptedTool builtins cannot use clap: no raw argv, and the logic-only profile is pub(crate) #2407
Description
Filed from everruns/everruns, where the everruns command tree has just moved its raw-argv adapter onto clap and the ScriptedTool adapter could not follow.
Context
bashkit already treats clap as the house parser for builtins: ClapBuiltin, bashkit::clap, and the clap_builtin / clap_builtin_subcommands examples. That applies to Bash builtins, which receive raw argv. ScriptedTool builtins cannot reach it.
What blocks it
A ScriptedTool tool is a ToolDef + callback, and the callback receives ToolArgs, which carries only the already-parsed params (param_str, param_i64, …). Raw argv is consumed by tool_def::parse_flags and never surfaced. ScriptedToolBuilder has no builtin() — that lives on BashBuilder.
Rebuilding the path on Bash::builder() downstream is not available either. ScriptedTool::create_bash does exactly that internally:
let mut builder = Bash::builder();
builder = builder.logic_only();
builder = builder.extension(ToolDefExtension::from_registered_tools(...));but BashBuilder::logic_only (src/lib.rs:2035) and interpreter::ShellProfile (src/interpreter/mod.rs:105) are both pub(crate). ToolDefExtension and BashBuilder::extension are public, so a consumer can reassemble everything except the filesystem-less profile — and that is the one part that cannot be given up. The scripted catalog's threat posture rests on filesystem primitives being rejected; a consumer-built Bash would hand a control-plane command surface a real filesystem to get better flag parsing, which is not a trade worth making.
What parse_flags gives up
tool_def::parse_flags is capable in places — schema-driven coercion, arrays, objects, --k=v, repeated flags. The gaps are the ones a CLI caller feels:
- Unknown flags are silently kept as strings (
src/tool_def.rs:353-, "Unknown flags (not in schema) are kept as strings").list_agents --limti 10parses, dispatches, and the argument is quietly dropped. This is the one I'd call a bug rather than a missing feature: a typo should not be indistinguishable from an omission. - No positionals —
expected --flag, got: {arg}(src/tool_def.rs:390). Downstream this forces a statement-boundary pre-rewriter that inserts--idbefore a bare word before the interpreter runs, because models typeget_agent <id>and hit a retry loop otherwise. - No short flags, no
--end-of-flags. - Required fields are not checked at parse time, so a missing one surfaces as a deserialization error from the far side of dispatch, without usage.
- No "did you mean", no per-command usage on error.
Possible shapes
Either would unblock it; the first is smaller.
ScriptedToolBuilder::builtin(name, Box<dyn Builtin>), mirroringBashBuilder::builtin, so a consumer can register a raw-argv builtin alongside theToolDeftools and parse it however it likes. Purely additive.- Make the logic-only profile public (
ExecutionProfile, or a publicShellProfile), so a consumer can assemble the filesystem-less shell from the publicBash::builder()+ToolDefExtensionpieces it already has.
Independently of either, treating an unknown flag as an error rather than as a string property looks worth doing inside parse_flags itself, perhaps opt-in on ToolDef to stay compatible.
Separate, smaller: ClapBuiltin assumes clap has no color feature
src/builtins/mod.rs, in the blanket impl<T: ClapBuiltin> Builtin for T:
// No `.color(ColorChoice::Never)` here: the workspace builds clap with
// `default-features = false`, so the `color` feature is absent and clap
// is unconditionally colourless. See the clap pin in the root Cargo.toml.That holds in bashkit's own workspace, but the feature set is not bashkit's to decide. Cargo unifies features across a build, so any consumer that links clap with default features — ours does, for its CLI — turns color on for bashkit's clap too, and ClapBuiltin and the coreutils builtins then emit ANSI into what is often an LLM tool result. .color(ColorChoice::Never) would need the color feature declared to be callable at all; anstream-free alternatives may be preferable. Happy to send a PR if you have a preferred shape.
Activity
claude commentedon Sep 15, 2026
Shipping this as #2408.
Taking shape (1) from the issue —
ScriptedToolBuilder::builtin(name, Box<dyn Builtin>)— since it is purely additive and keepslogic_onlyprivate, so the filesystem-less profile stays non-negotiable rather than becoming something a consumer assembles for themselves.On unknown flags: rather than a new
ToolDeffield,parse_flagsnow honours"additionalProperties": false, which is the ruleToolRegistryalready applies to structured calls (tool_registry.rs:737). Same schema, same meaning on both surfaces, and still opt-in as you suggested.On the
ClapBuiltincolour note — your diagnosis of the comment is right, the feature set is not bashkit's to decide. Worth recording that the consequence is milder than it looks: clap'sDisplay for StyledStrrunsanstream::adapter::strip_strwhencoloris on, so output is colourless today even under unification. That is an internal detail of clap's rendering rather than a contract, so #2408 strips on our side too and corrects the comment..color(ColorChoice::Never)is not usable as the fix: the method does not exist without thecolorfeature, so calling it would break the colourless build.Positionals/short flags/
--/required-arg checking are all reachable through clap now, soparse_flagsis left as the schema-driven parser it is rather than growing a second CLI grammar.Generated by Claude Code
feat(scripted_tool): let ScriptedTool builtins use clap (#2408)