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

  1. Remove `#[group(skip)]` from the inner `Args` struct so it exposes a group id, keeping `#[clap(flatten)] args: Option<InnerArgs>` working.
  2. 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.
  3. 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

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.