clap-rs/clap · error
cannot `#[flatten]` an `Option<Args>` with `#[group(skip)]`
Error message
cannot `#[flatten]` an `Option<Args>` with `#[group(skip)]`
What it means
This is a runtime `.expect()` panic emitted into the generated derive code at `clap_derive/src/derives/args.rs:233`. For a field annotated `#[clap(flatten)]` whose type is `Option<T>` (where `T: clap::Args`), the derive emits `<T as clap::Args>::group_id().expect(...)`. `group_id()` returns `None` when the inner `Args` struct carries `#[group(skip)]`, so the `.expect()` panics. Clap throws it because flattening an *optional* group requires a real group id to attach the "present-or-absent" semantics; with the group skipped there is no id to bind optionality to, so the combination is unsupported.
Source
Thrown at clap_derive/src/derives/args.rs:233
Some(quote! {
let #app_var = <#subcmd_type as clap::Subcommand>::augment_subcommands( #app_var );
let #app_var = #app_var
#implicit_methods
#override_methods;
})
}
Kind::Flatten(ty) => {
let inner_type = match (**ty, sub_type(&field.ty)) {
(Ty::Option, Some(sub_type)) => sub_type,
_ => &field.ty,
};
let next_help_heading = item.next_help_heading();
let next_display_order = item.next_display_order();
let flatten_group_assert = if matches!(**ty, Ty::Option) {
quote_spanned! { kind.span()=>
<#inner_type as clap::Args>::group_id().expect("cannot `#[flatten]` an `Option<Args>` with `#[group(skip)]`");
}
} else {
quote! {}
};
if override_required {
Some(quote_spanned! { kind.span()=>
#flatten_group_assert
let #app_var = #app_var
#next_help_heading
#next_display_order;
let #app_var = <#inner_type as clap::Args>::augment_args_for_update(#app_var);
})
} else {
Some(quote_spanned! { kind.span()=>
#flatten_group_assert
let #app_var = #app_var
#next_help_heading
#next_display_order;View on GitHub (pinned to ccc92c3b11)
Solutions
- Remove `#[group(skip)]` from the inner `Args` struct so it exposes a group id, keeping `#[clap(flatten)] args: Option<InnerArgs>` working.
- Drop the `Option<...>` wrapper and flatten the inner `Args` directly (e.g. `#[clap(flatten)] args: InnerArgs`); make individual fields optional instead of the whole group.
- If the whole group must be conditionally absent and skipped, stop using `#[flatten]` and model it as a `#[command(subcommand)]` or a manual `Option<InnerArgs>` field parsed by value, so clap does not need the inner group id.
Example fix
// before
#[derive(clap::Parser)]
struct Cli {
#[clap(flatten)]
args: Option<Args>,
}
#[derive(clap::Args)]
#[group(skip)]
struct Args {
#[clap(short)]
param: bool,
}
// after — give the inner Args a group id by removing the skip
#[derive(clap::Parser)]
struct Cli {
#[clap(flatten)]
args: Option<Args>,
}
#[derive(clap::Args)]
struct Args {
#[clap(short)]
param: bool,
} Defensive patterns
Strategy: validation
Validate before calling
// Pre-flight check (run in a #[test] or at startup). For a #[command(flatten)] field of
// type Option<T>, clap_derive emits the runtime call
// <T as clap::Args>::group_id().expect("cannot `#[flatten]` an `Option<Args>` with `#[group(skip)]`")
// which panics while building the Command (during parse/try_parse) when the inner Args has
// #[group(skip)] (group_id() == None). Verify the precondition explicitly:
fn assert_flattenable_optional_args<T: clap::Args>(name: &str) {
assert!(
<T as clap::Args>::group_id().is_some(),
"{} has #[group(skip)] and cannot be used as Option<{}> with #[command(flatten)]",
name,
name
);
}
#[test]
fn flatten_targets_expose_a_group() {
// list every inner Args type you flatten behind Option<T>:
assert_flattenable_optional_args::<MyInnerArgs>("MyInnerArgs");
} Type guard
// True => T: Args is safe to flatten as Option<T>.
// False => T carries #[group(skip)] and parse() will panic.
fn is_flattenable_optional_args<T: clap::Args>() -> bool {
<T as clap::Args>::group_id().is_some()
} Try / catch
// The panic occurs at runtime while augmenting the Command (inside parse/parse_from),
// so it can be intercepted with catch_unwind as a last-resort guard.
use std::panic::{self, AssertUnwindSafe};
let cli = match panic::catch_unwind(AssertUnwindSafe(|| Cli::parse_from(std::env::args()))) {
Ok(cli) => cli,
Err(_payload) => {
// Most common cause: #[command(flatten)] Option<Args> whose inner type has #[group(skip)].
eprintln!("CLI misconfigured: cannot `#[flatten]` an `Option<Args>` with `#[group(skip)]`");
std::process::exit(2);
}
}; Prevention
- Never combine `#[command(flatten)]` on an `Option<T>` field with `#[group(skip)]` on the inner `T: Args`; the generated code calls `group_id().expect(...)` at runtime.
- Prefer making the flattened field required (`inner: T` instead of `Option<T>`) — clap only emits the `group_id().expect()` guard for the `Option<Args>` case.
- If the inner struct genuinely needs `#[group(skip)]`, do not flatten it behind `Option`: flatten it as a required `Args` or model it as a separate argument/subcommand.
- Add a unit test that builds `Cli::command()` (or asserts `<T as Args>::group_id().is_some()` for each flatten target) so the misconfiguration fails in CI rather than at first run.
- When upgrading clap, grep your CLI structs for the combination of `flatten` and `group(skip)`, since attribute-compatibility rules can change between versions.
When it happens
Trigger: Define `#[derive(clap::Parser)] struct Cli { #[clap(flatten)] args: Option<InnerArgs> }` where `InnerArgs` is `#[derive(clap::Args)]` and is annotated with `#[group(skip)]` (see the regression test `flatten_skipped_group` in `tests/derive/flatten.rs:288-305`). The panic occurs at parse time when the derived `augment_args`/`augment_args_for_update` runs (e.g. `Cli::parse()`, `Cli::try_parse_from(...)`). Wrapping in `Option<Box<InnerArgs>>` triggers the same path via `sub_type`.
Common situations: Splitting a large CLI into sub-structs that are reused, marking one `#[group(skip)]` to avoid an extra arg group in help, then trying to make it optional via `Option<...>` and flattening it into the parent. Refactors that promote a flattened `Args` to `Option<Args>` for conditional configuration, or copying a `#[group(skip)]` struct that worked elsewhere into an `Option`-wrapped flatten, both hit this. The message was improved in the 4.x series (see CHANGELOG) but the panic remains.
Related errors
AI-assisted analysis of clap-rs/clap@ccc92c3b11 (2026-08-01).
Data as JSON: /data/errors/081fe0fd7b3956c0.json.
Report an issue: GitHub.