Repository navigation
feat(cli): add PATH-based plugin discovery for external subcommands #1851
Description
Problem Statement
OpenShell CLI should be extensible via third-party plugins, following the same pattern used by git, kubectl, docker, and helm. If a binary named openshell-<name> exists on $PATH, then openshell <name> [args...] should discover and execute it, forwarding all remaining arguments. This enables the ecosystem to grow beyond built-in commands without requiring changes to the core CLI.
Technical Context
The CLI binary is named openshell (defined in crates/openshell-cli/Cargo.toml). It uses clap 4.x with #[derive(Parser)] and a Commands enum with 14 subcommand variants. Today, unknown subcommands produce a clap error (unrecognized subcommand) before any custom code runs. Clap 4.x has first-class support for external subcommands via allow_external_subcommands and an #[command(external_subcommand)] enum variant, making this a well-supported pattern.
Affected Components
| Component | Key Files | Role |
|---|---|---|
| CLI entry point | crates/openshell-cli/src/main.rs |
Parser struct, Commands enum, dispatch match |
| Help template | crates/openshell-cli/src/main.rs:197-233 |
Custom grouped help text |
| Process exec patterns | crates/openshell-cli/src/ssh.rs:218-242 |
Existing exec_or_wait pattern to reuse |
| CLI tests | crates/openshell-cli/src/main.rs:3124-4575 |
~90 parse/completion unit tests |
Technical Investigation
Architecture Overview
main()
├── rustls crypto provider install
├── CompleteEnv::with_factory(Cli::command).complete() // shell completions
├── Cli::parse() // clap parses args
├── TlsOptions setup from global flags
├── tracing_subscriber init from --verbose
└── match cli.command {
Some(Commands::Gateway { .. }) => ...
Some(Commands::Sandbox { .. }) => ...
...14 variants...
None => print root help ◄── insertion point
}
Key clap configuration:
command: Option<Commands>— subcommand is optionaldisable_help_subcommand = true,disable_help_flag = true— help manually managedallow_external_subcommandsis not currently set- Custom
HELP_TEMPLATEgroups commands into "SANDBOX COMMANDS", "GATEWAY COMMANDS", "ADDITIONAL COMMANDS"
Code References
| Location | Description |
|---|---|
crates/openshell-cli/Cargo.toml:14 |
Binary name: openshell |
crates/openshell-cli/src/main.rs:356-408 |
Cli struct with #[derive(Parser)] |
crates/openshell-cli/src/main.rs:410-580 |
Commands enum with #[derive(Subcommand)] — 14 variants |
crates/openshell-cli/src/main.rs:197-233 |
HELP_TEMPLATE — custom grouped help |
crates/openshell-cli/src/main.rs:1887-3106 |
Main dispatch: match cli.command |
crates/openshell-cli/src/main.rs:3100-3102 |
None arm — prints help for no subcommand |
crates/openshell-cli/src/ssh.rs:218-242 |
exec_or_wait() — Unix process replacement pattern |
crates/openshell-cli/src/ssh.rs:1472-1492 |
launch_editor_command() — spawn with NotFound handling |
crates/openshell-cli/src/main.rs:3124-4575 |
Test module — ~90 parse/completion tests |
Current Behavior
When a user types openshell foobar:
Cli::parse()calls clap's parser- Clap sees
foobardoesn't match anyCommandsvariant - Clap prints
error: unrecognized subcommand 'foobar'with suggestions and exits code 2 - Custom code in
main()is never reached
What Would Need to Change
1. Clap configuration (Cli struct):
- Add
allow_external_subcommands = trueto the#[command(...)]attribute - Add an
#[command(external_subcommand)] External(Vec<OsString>)variant to theCommandsenum
2. Plugin dispatch (new match arm in main()):
- Extract the subcommand name and remaining args from the
External(Vec<OsString>)variant - Look up
openshell-<subcommand>on$PATH(viawhichcrate orCommand::new+ error matching) - Set environment variables for global CLI context (gateway name, endpoint, verbose level)
- On Unix: use
exec()to replace the process (matches existingexec_or_waitpattern) - On non-Unix: spawn and forward exit code
3. Internal binary blocklist:
- The workspace ships binaries like
openshell-gateway,openshell-sandbox,openshell-driver-kubernetes, etc. - These are on
$PATHin deployment scenarios but are NOT plugins - Need a blocklist to prevent
openshell driver-kubernetesfrom exec'ing the driver binary
4. Help template update:
- Add a "PLUGINS" section to
HELP_TEMPLATEexplaining thatopenshell-<name>binaries on PATH are available asopenshell <name>
Alternative Approaches Considered
| Approach | Pros | Cons | Verdict |
|---|---|---|---|
A. allow_external_subcommands |
Built into clap, minimal code, established pattern | Changes error messages for genuine typos | Recommended |
| B. Manual pre-parse | No clap changes, preserves error messages | Must handle global flags manually, fragile | Not recommended |
C. try_parse + fallback |
Preserves clap error messages for typos | More complex; must re-handle parse failures | Good fallback if typo UX matters |
Patterns to Follow
- Process replacement: Reuse the
exec_or_waitpattern fromssh.rs:218-242— Unixexec()for clean signal handling, Windows spawn+wait fallback - Error handling: Reuse the
NotFoundmatching pattern fromlaunch_editor_commandinssh.rs:1472-1492 - Env var conventions: Follow existing
OPENSHELL_*env var naming (gateway, gateway_endpoint, gateway_insecure)
Proposed Approach
Use clap's built-in allow_external_subcommands + #[command(external_subcommand)] enum variant. When an unknown subcommand is encountered, look up openshell-<name> on $PATH, propagate global CLI state as environment variables, and exec() the plugin on Unix (spawn+wait on Windows). Add a blocklist for internal binaries (openshell-gateway, openshell-sandbox, etc.) that are not plugins. Update the help template with a "PLUGINS" section.
Scope Assessment
- Complexity: Low
- Confidence: High — clear path, well-understood pattern, clap has first-class support
- Estimated files to change: 2-3 (main.rs, Cargo.toml for
whichdep, possibly a new test file) - Issue type:
feat
Risks & Open Questions
- Internal binary collision: Binaries like
openshell-gatewayandopenshell-sandboxare on PATH in deployments. A blocklist is needed, but the exact list should be reviewed — should it be hardcoded or derived from workspaceCargo.toml? - Auth token forwarding: Should plugins receive auth tokens via environment variables? This widens the trust boundary. Recommendation: do NOT pass tokens; let plugins resolve auth via
openshell-bootstrapAPIs. Needs human decision. - Shell completions: External subcommands won't appear in shell completions automatically. The
clap_completedynamic engine won't suggest plugin names. Acceptable for v1, but worth noting as a follow-up. - Error UX for typos: With
allow_external_subcommands, clap no longer shows "did you mean?" suggestions for typos. The plugin-not-found error should include available built-in commands or suggest--help. Alternatively, use thetry_parsefallback approach (Option C). - Security (accepted risk): Running arbitrary binaries from PATH is the same threat model as git/kubectl plugins — the user controls their PATH. No shell interpolation is used (
Command::newis safe).
Test Considerations
- Unit tests: Add parse tests verifying (1) external subcommands are captured in the
Externalvariant, (2) built-in commands take precedence, (3) global flags work with external subcommands - Integration test: Create a temporary
openshell-test-pluginscript on PATH, runopenshell test-plugin --check, verify exit code and environment variables are forwarded - Existing tests: The ~90 existing parse/completion tests in
main.rs::testsmust continue to pass. Thecompletions_engine_returns_candidatestest should verify external subcommands don't leak into normal completion - Blocklist test: Verify that
openshell driver-kubernetes(internal binary name) produces an error, not a plugin exec
Created by spike investigation. Use build-from-issue to plan and implement.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
TaylorMutch commentedon Jun 10, 2026
This is an interesting idea! Can you expand on the kinds of use cases you have in mind?
johntmyers commentedon Jun 15, 2026
@benoitf we want to prioritize extendability features so am curious to get more feedback here. Also do you see a pathway to doing something similar for SDK interfaces (we still need our core SDK built out as well).
benoitf commentedon Jun 15, 2026
hello @TaylorMutch , @johntmyers
the specific usage I had in my mind was to have an image builder fitting the need of my project. For now there are some pre-existing images or you need to build your own images but there is room for an intermediate way of like taking a devContainer requirement and building that image (+including the agent), [1] etc.
But usually even without this specific case, IMHO there are usually some utilities/helpers that people want to bring in but as it may target only some specific drivers/use-cases it can't fit in a global sdk, but then having it integrated as part of the main cli allow an easy installation
[1] there is an existing project there
https://github.com/openkaiden/openshell-image-builder
github-actions commentedon Jun 30, 2026
This issue has had no activity for 14 days and is now marked stale. It may be closed in 7 days if there is no further activity. Comment or remove the state:stale label to keep it open.