Skip to main content

safe_chains/handlers/
mod.rs

1pub mod ai;
2pub mod android;
3pub mod containers;
4pub mod coreutils;
5pub mod dotnet;
6pub mod forges;
7pub mod go;
8pub mod jvm;
9pub mod magick;
10pub mod network;
11pub mod node;
12pub mod perl;
13pub mod php;
14pub mod python;
15pub mod r;
16pub mod ruby;
17pub mod rust;
18pub mod shell;
19pub mod swift;
20pub mod system;
21pub mod vcs;
22pub mod wrappers;
23pub mod xcode;
24
25use crate::parse::Token;
26use crate::verdict::Verdict;
27
28pub fn dispatch(tokens: &[Token]) -> Verdict {
29    let cmd = tokens[0].command_name();
30    None
31        .or_else(|| shell::dispatch(cmd, tokens))
32        .or_else(|| wrappers::dispatch(cmd, tokens))
33        .or_else(|| vcs::dispatch(cmd, tokens))
34        .or_else(|| forges::dispatch(cmd, tokens))
35        .or_else(|| node::dispatch(cmd, tokens))
36        .or_else(|| ruby::dispatch(cmd, tokens))
37        .or_else(|| python::dispatch(cmd, tokens))
38        .or_else(|| rust::dispatch(cmd, tokens))
39        .or_else(|| go::dispatch(cmd, tokens))
40        .or_else(|| jvm::dispatch(cmd, tokens))
41        .or_else(|| android::dispatch(cmd, tokens))
42        .or_else(|| php::dispatch(cmd, tokens))
43        .or_else(|| swift::dispatch(cmd, tokens))
44        .or_else(|| dotnet::dispatch(cmd, tokens))
45        .or_else(|| containers::dispatch(cmd, tokens))
46        .or_else(|| network::dispatch(cmd, tokens))
47        .or_else(|| ai::dispatch(cmd, tokens))
48        .or_else(|| system::dispatch(cmd, tokens))
49        .or_else(|| xcode::dispatch(cmd, tokens))
50        .or_else(|| perl::dispatch(cmd, tokens))
51        .or_else(|| r::dispatch(cmd, tokens))
52        .or_else(|| coreutils::dispatch(cmd, tokens))
53        .or_else(|| magick::dispatch(cmd, tokens))
54        .unwrap_or(Verdict::Denied)
55}
56
57#[cfg(test)]
58const HANDLED_CMDS: &[&str] = &[
59    "sh", "bash", "xargs", "timeout", "time", "env", "nice", "ionice", "hyperfine", "dotenv",
60    "git", "jj", "gh", "glab", "jjpr", "tea",
61    "npm", "yarn", "pnpm", "bun", "deno", "npx", "bunx", "nvm", "fnm", "volta",
62    "ruby", "ri", "bundle", "gem", "importmap", "rbenv",
63    "pip", "uv", "poetry", "pyenv", "conda",
64    "cargo", "rustup",
65    "go",
66    "gradle", "mvn", "mvnw", "ktlint", "detekt",
67    "javap", "jar", "keytool", "jarsigner",
68    "adb", "apkanalyzer", "apksigner", "bundletool", "aapt2",
69    "emulator", "avdmanager", "sdkmanager", "zipalign", "lint",
70    "fastlane", "firebase",
71    "composer", "craft",
72    "swift",
73    "dotnet",
74    "curl",
75    "docker", "podman", "kubectl", "orbctl", "qemu-img",
76    "ollama", "llm", "hf", "claude", "aider", "codex", "opencode", "vibe",
77    "ddev", "dcli",
78    "brew", "mise", "asdf", "crontab", "defaults", "pmset", "sysctl", "cmake", "psql", "pg_isready",
79    "terraform", "heroku", "vercel", "flyctl",
80    "networksetup", "launchctl", "diskutil", "security", "csrutil", "log",
81    "xcodebuild", "plutil", "xcode-select", "xcrun", "pkgutil", "lipo", "codesign", "spctl",
82    "xcodegen", "tuist", "pod", "swiftlint", "swiftformat", "periphery", "xcbeautify", "agvtool", "simctl",
83    "perl",
84    "R", "Rscript",
85    "grep", "rg", "ag", "ack", "zgrep", "locate",
86    "cat", "head", "tail", "wc", "cut", "tr", "uniq", "less", "more", "zcat",
87    "diff", "comm", "paste", "tac", "rev", "nl",
88    "expand", "unexpand", "fold", "fmt", "col", "column", "iconv", "nroff",
89    "echo", "printf", "seq", "test", "expr", "bc", "factor", "bat",
90    "arch", "command", "hostname",
91    "find", "sed", "shuf", "sort", "yq", "xmllint", "awk", "gawk", "mawk", "nawk",
92    "magick",
93    "fd", "eza", "ls", "delta", "colordiff",
94    "dirname", "basename", "realpath", "readlink",
95    "file", "stat", "du", "df", "tree", "cmp", "zipinfo", "tar", "unzip", "gzip",
96    "true", "false",
97    "alias", "export", "printenv", "read", "type", "wait", "whereis", "which", "whoami", "date", "pwd", "cd", "unset",
98    "uname", "nproc", "uptime", "id", "groups", "tty", "locale", "cal", "sleep",
99    "who", "w", "last", "lastlog",
100    "ps", "top", "htop", "iotop", "procs", "dust", "lsof", "pgrep", "lsblk", "free",
101    "jq", "base64", "xxd", "getconf", "uuidgen",
102    "md5sum", "md5", "sha256sum", "shasum", "sha1sum", "sha512sum",
103    "cksum", "b2sum", "sum", "strings", "hexdump", "od", "size", "sips",
104    "sw_vers", "mdls", "otool", "nm", "system_profiler", "ioreg", "vm_stat", "mdfind", "man",
105    "dig", "nslookup", "host", "whois", "netstat", "ss", "ifconfig", "route", "ping",
106    "identify", "shellcheck", "cloc", "tokei", "cucumber", "branchdiff", "workon", "safe-chains",
107];
108
109pub fn handler_docs() -> Vec<crate::docs::CommandDoc> {
110    let mut docs = Vec::new();
111    docs.extend(vcs::command_docs());
112    docs.extend(forges::command_docs());
113    docs.extend(node::command_docs());
114    docs.extend(ruby::command_docs());
115    docs.extend(python::command_docs());
116    docs.extend(rust::command_docs());
117    docs.extend(go::command_docs());
118    docs.extend(jvm::command_docs());
119    docs.extend(android::command_docs());
120    docs.extend(php::command_docs());
121    docs.extend(swift::command_docs());
122    docs.extend(dotnet::command_docs());
123    docs.extend(containers::command_docs());
124    docs.extend(ai::command_docs());
125    docs.extend(network::command_docs());
126    docs.extend(system::command_docs());
127    docs.extend(xcode::command_docs());
128    docs.extend(perl::command_docs());
129    docs.extend(r::command_docs());
130    docs.extend(coreutils::command_docs());
131    docs.extend(shell::command_docs());
132    docs.extend(wrappers::command_docs());
133    docs.extend(magick::command_docs());
134    docs
135}
136
137#[cfg(test)]
138#[derive(Debug)]
139pub(crate) enum CommandEntry {
140    Positional { cmd: &'static str },
141    Custom { cmd: &'static str, valid_prefix: Option<&'static str> },
142    Subcommand { cmd: &'static str, subs: &'static [SubEntry], bare_ok: bool },
143    Delegation { cmd: &'static str },
144}
145
146#[cfg(test)]
147#[derive(Debug)]
148pub(crate) enum SubEntry {
149    Policy { name: &'static str },
150    Nested { name: &'static str, subs: &'static [SubEntry] },
151    Custom { name: &'static str, valid_suffix: Option<&'static str> },
152    Positional,
153    Guarded { name: &'static str, valid_suffix: &'static str },
154}
155
156use crate::command::CommandDef;
157
158const COMMAND_DEFS: &[&CommandDef] = &[
159    &ai::CODEX, &ai::OLLAMA, &ai::OPENCODE, &ai::LLM, &ai::HF,
160    &containers::DOCKER, &containers::PODMAN, &containers::KUBECTL, &containers::ORBCTL, &containers::QEMU_IMG,
161    &dotnet::DOTNET,
162    &go::GO,
163    &android::APKANALYZER, &android::APKSIGNER, &android::BUNDLETOOL, &android::AAPT2,
164    &android::AVDMANAGER,
165    &jvm::GRADLE, &jvm::KEYTOOL,
166    &magick::MAGICK,
167    &node::NPM, &node::PNPM, &node::BUN, &node::DENO,
168    &node::NVM, &node::FNM, &node::VOLTA,
169    &php::COMPOSER, &php::CRAFT,
170    &python::PIP, &python::UV, &python::POETRY,
171    &python::PYENV, &python::CONDA,
172    &ruby::BUNDLE, &ruby::GEM, &ruby::IMPORTMAP, &ruby::RBENV,
173    &rust::CARGO, &rust::RUSTUP,
174    &vcs::GIT,
175    &swift::SWIFT,
176    &system::BREW, &system::MISE, &system::ASDF, &system::DDEV, &system::DCLI, &system::CMAKE,
177    &system::DEFAULTS, &system::TERRAFORM, &system::HEROKU, &system::VERCEL,
178    &system::FLYCTL, &system::FASTLANE, &system::FIREBASE,
179    &system::SECURITY, &system::CSRUTIL, &system::DISKUTIL,
180    &system::LAUNCHCTL, &system::LOG,
181    &xcode::XCODEBUILD, &xcode::PLUTIL, &xcode::XCODE_SELECT,
182    &xcode::XCODEGEN, &xcode::TUIST, &xcode::POD, &xcode::SWIFTLINT,
183    &xcode::PERIPHERY, &xcode::AGVTOOL, &xcode::SIMCTL,
184];
185
186pub fn all_opencode_patterns() -> Vec<String> {
187    let mut patterns = Vec::new();
188    for def in COMMAND_DEFS {
189        patterns.extend(def.opencode_patterns());
190    }
191    for def in coreutils::all_flat_defs() {
192        patterns.extend(def.opencode_patterns());
193    }
194    for def in jvm::jvm_flat_defs() {
195        patterns.extend(def.opencode_patterns());
196    }
197    for def in android::android_flat_defs() {
198        patterns.extend(def.opencode_patterns());
199    }
200    for def in ai::ai_flat_defs() {
201        patterns.extend(def.opencode_patterns());
202    }
203    for def in ruby::ruby_flat_defs() {
204        patterns.extend(def.opencode_patterns());
205    }
206    for def in system::system_flat_defs() {
207        patterns.extend(def.opencode_patterns());
208    }
209    for def in xcode::xcbeautify_flat_defs() {
210        patterns.extend(def.opencode_patterns());
211    }
212    patterns.sort();
213    patterns.dedup();
214    patterns
215}
216
217#[cfg(test)]
218fn full_registry() -> Vec<&'static CommandEntry> {
219    let mut entries = Vec::new();
220    entries.extend(shell::REGISTRY);
221    entries.extend(wrappers::REGISTRY);
222    entries.extend(vcs::full_registry());
223    entries.extend(forges::full_registry());
224    entries.extend(node::full_registry());
225    entries.extend(jvm::full_registry());
226    entries.extend(android::full_registry());
227    entries.extend(network::REGISTRY);
228    entries.extend(system::full_registry());
229    entries.extend(xcode::full_registry());
230    entries.extend(perl::REGISTRY);
231    entries.extend(r::REGISTRY);
232    entries.extend(coreutils::full_registry());
233    entries
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use std::collections::HashSet;
240
241    const UNKNOWN_FLAG: &str = "--xyzzy-unknown-42";
242    const UNKNOWN_SUB: &str = "xyzzy-unknown-42";
243
244    fn check_entry(entry: &CommandEntry, failures: &mut Vec<String>) {
245        match entry {
246            CommandEntry::Positional { .. } | CommandEntry::Delegation { .. } => {}
247            CommandEntry::Custom { cmd, valid_prefix } => {
248                let base = valid_prefix.unwrap_or(cmd);
249                let test = format!("{base} {UNKNOWN_FLAG}");
250                if crate::is_safe_command(&test) {
251                    failures.push(format!("{cmd}: accepted unknown flag: {test}"));
252                }
253            }
254            CommandEntry::Subcommand { cmd, subs, bare_ok } => {
255                if !bare_ok && crate::is_safe_command(cmd) {
256                    failures.push(format!("{cmd}: accepted bare invocation"));
257                }
258                let test = format!("{cmd} {UNKNOWN_SUB}");
259                if crate::is_safe_command(&test) {
260                    failures.push(format!("{cmd}: accepted unknown subcommand: {test}"));
261                }
262                for sub in *subs {
263                    check_sub(cmd, sub, failures);
264                }
265            }
266        }
267    }
268
269    fn check_sub(prefix: &str, entry: &SubEntry, failures: &mut Vec<String>) {
270        match entry {
271            SubEntry::Policy { name } => {
272                let test = format!("{prefix} {name} {UNKNOWN_FLAG}");
273                if crate::is_safe_command(&test) {
274                    failures.push(format!("{prefix} {name}: accepted unknown flag: {test}"));
275                }
276            }
277            SubEntry::Nested { name, subs } => {
278                let path = format!("{prefix} {name}");
279                let test = format!("{path} {UNKNOWN_SUB}");
280                if crate::is_safe_command(&test) {
281                    failures.push(format!("{path}: accepted unknown subcommand: {test}"));
282                }
283                for sub in *subs {
284                    check_sub(&path, sub, failures);
285                }
286            }
287            SubEntry::Custom { name, valid_suffix } => {
288                let base = match valid_suffix {
289                    Some(s) => format!("{prefix} {name} {s}"),
290                    None => format!("{prefix} {name}"),
291                };
292                let test = format!("{base} {UNKNOWN_FLAG}");
293                if crate::is_safe_command(&test) {
294                    failures.push(format!("{prefix} {name}: accepted unknown flag: {test}"));
295                }
296            }
297            SubEntry::Positional => {}
298            SubEntry::Guarded { name, valid_suffix } => {
299                let test = format!("{prefix} {name} {valid_suffix} {UNKNOWN_FLAG}");
300                if crate::is_safe_command(&test) {
301                    failures.push(format!("{prefix} {name}: accepted unknown flag: {test}"));
302                }
303            }
304        }
305    }
306
307    #[test]
308    fn all_commands_reject_unknown() {
309        let registry = full_registry();
310        let mut failures = Vec::new();
311        for entry in &registry {
312            check_entry(entry, &mut failures);
313        }
314        assert!(
315            failures.is_empty(),
316            "unknown flags/subcommands accepted:\n{}",
317            failures.join("\n")
318        );
319    }
320
321    #[test]
322    fn command_defs_reject_unknown() {
323        for def in COMMAND_DEFS {
324            def.auto_test_reject_unknown();
325        }
326    }
327
328    #[test]
329    fn flat_defs_reject_unknown() {
330        for def in coreutils::all_flat_defs() {
331            def.auto_test_reject_unknown();
332        }
333        for def in xcode::xcbeautify_flat_defs() {
334            def.auto_test_reject_unknown();
335        }
336        for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()) {
337            def.auto_test_reject_unknown();
338        }
339    }
340
341
342    #[test]
343    fn bare_false_rejects_bare_invocation() {
344        let check_def = |def: &crate::command::FlatDef| {
345            if !def.policy.bare {
346                assert!(
347                    !crate::is_safe_command(def.name),
348                    "{}: bare=false but bare invocation accepted",
349                    def.name,
350                );
351            }
352        };
353        for def in coreutils::all_flat_defs()
354            .into_iter()
355            .chain(xcode::xcbeautify_flat_defs())
356        {
357            check_def(def);
358        }
359        for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()) {
360            check_def(def);
361        }
362    }
363
364    fn visit_subs(prefix: &str, subs: &[crate::command::SubDef], visitor: &mut dyn FnMut(&str, &crate::command::SubDef)) {
365        for sub in subs {
366            visitor(prefix, sub);
367            if let crate::command::SubDef::Nested { name, subs: inner } = sub {
368                visit_subs(&format!("{prefix} {name}"), inner, visitor);
369            }
370        }
371    }
372
373    #[test]
374    fn guarded_subs_require_guard() {
375        let mut failures = Vec::new();
376        for def in COMMAND_DEFS {
377            visit_subs(def.name, def.subs, &mut |prefix, sub| {
378                if let crate::command::SubDef::Guarded { name, guard_long, .. } = sub {
379                    let without = format!("{prefix} {name}");
380                    if crate::is_safe_command(&without) {
381                        failures.push(format!("{without}: accepted without guard {guard_long}"));
382                    }
383                    let with = format!("{prefix} {name} {guard_long}");
384                    if !crate::is_safe_command(&with) {
385                        failures.push(format!("{with}: rejected with guard {guard_long}"));
386                    }
387                }
388            });
389        }
390        assert!(failures.is_empty(), "guarded sub issues:\n{}", failures.join("\n"));
391    }
392
393    #[test]
394    fn guarded_subs_accept_guard_short() {
395        let mut failures = Vec::new();
396        for def in COMMAND_DEFS {
397            visit_subs(def.name, def.subs, &mut |prefix, sub| {
398                if let crate::command::SubDef::Guarded { name, guard_short: Some(short), .. } = sub {
399                    let with_short = format!("{prefix} {name} {short}");
400                    if !crate::is_safe_command(&with_short) {
401                        failures.push(format!("{with_short}: rejected with guard_short"));
402                    }
403                }
404            });
405        }
406        assert!(failures.is_empty(), "guard_short issues:\n{}", failures.join("\n"));
407    }
408
409    #[test]
410    fn nested_subs_reject_bare() {
411        let mut failures = Vec::new();
412        for def in COMMAND_DEFS {
413            visit_subs(def.name, def.subs, &mut |prefix, sub| {
414                if let crate::command::SubDef::Nested { name, .. } = sub {
415                    let bare = format!("{prefix} {name}");
416                    if crate::is_safe_command(&bare) {
417                        failures.push(format!("{bare}: nested sub accepted bare invocation"));
418                    }
419                }
420            });
421        }
422        assert!(failures.is_empty(), "nested bare issues:\n{}", failures.join("\n"));
423    }
424
425    #[test]
426    fn process_substitution_blocked() {
427        let cmds = ["echo <(cat /etc/passwd)", "echo >(rm -rf /)", "grep pattern <(ls)"];
428        for cmd in &cmds {
429            assert!(
430                !crate::is_safe_command(cmd),
431                "process substitution not blocked: {cmd}",
432            );
433        }
434    }
435
436    #[test]
437    fn positional_style_accepts_unknown_args() {
438        use crate::policy::FlagStyle;
439        for def in coreutils::all_flat_defs() {
440            if def.policy.flag_style == FlagStyle::Positional {
441                let test = format!("{} --unknown-xyz", def.name);
442                assert!(
443                    crate::is_safe_command(&test),
444                    "{}: FlagStyle::Positional but rejected unknown arg",
445                    def.name,
446                );
447            }
448        }
449    }
450
451    fn visit_policies(prefix: &str, subs: &[crate::command::SubDef], visitor: &mut dyn FnMut(&str, &crate::policy::FlagPolicy)) {
452        for sub in subs {
453            match sub {
454                crate::command::SubDef::Policy { name, policy, .. } => {
455                    visitor(&format!("{prefix} {name}"), policy);
456                }
457                crate::command::SubDef::Guarded { name, guard_long, policy, .. } => {
458                    visitor(&format!("{prefix} {name} {guard_long}"), policy);
459                }
460                crate::command::SubDef::Nested { name, subs: inner } => {
461                    visit_policies(&format!("{prefix} {name}"), inner, visitor);
462                }
463                _ => {}
464            }
465        }
466    }
467
468    #[test]
469    fn valued_flags_accept_eq_syntax() {
470        let mut failures = Vec::new();
471
472        let check_flat = |def: &crate::command::FlatDef, failures: &mut Vec<String>| {
473            for flag in def.policy.valued.iter() {
474                let cmd = format!("{} {flag}=test_value", def.name);
475                if !crate::is_safe_command(&cmd) {
476                    failures.push(format!("{cmd}: valued flag rejected with = syntax"));
477                }
478            }
479        };
480        for def in coreutils::all_flat_defs()
481            .into_iter()
482            .chain(xcode::xcbeautify_flat_defs())
483        {
484            check_flat(def, &mut failures);
485        }
486        for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()) {
487            check_flat(def, &mut failures);
488        }
489
490        for def in COMMAND_DEFS {
491            visit_policies(def.name, def.subs, &mut |prefix, policy| {
492                for flag in policy.valued.iter() {
493                    let cmd = format!("{prefix} {flag}=test_value");
494                    if !crate::is_safe_command(&cmd) {
495                        failures.push(format!("{cmd}: valued flag rejected with = syntax"));
496                    }
497                }
498            });
499        }
500
501        assert!(failures.is_empty(), "valued = syntax issues:\n{}", failures.join("\n"));
502    }
503
504    #[test]
505    fn max_positional_enforced() {
506        let mut failures = Vec::new();
507
508        let check_flat = |def: &crate::command::FlatDef, failures: &mut Vec<String>| {
509            if let Some(max) = def.policy.max_positional {
510                let args: Vec<&str> = (0..=max).map(|_| "testarg").collect();
511                let cmd = format!("{} {}", def.name, args.join(" "));
512                if crate::is_safe_command(&cmd) {
513                    failures.push(format!(
514                        "{}: max_positional={max} but accepted {} positional args",
515                        def.name,
516                        max + 1,
517                    ));
518                }
519            }
520        };
521        for def in coreutils::all_flat_defs()
522            .into_iter()
523            .chain(xcode::xcbeautify_flat_defs())
524        {
525            check_flat(def, &mut failures);
526        }
527        for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()) {
528            check_flat(def, &mut failures);
529        }
530
531        for def in COMMAND_DEFS {
532            visit_policies(def.name, def.subs, &mut |prefix, policy| {
533                if let Some(max) = policy.max_positional {
534                    let args: Vec<&str> = (0..=max).map(|_| "testarg").collect();
535                    let cmd = format!("{prefix} {}", args.join(" "));
536                    if crate::is_safe_command(&cmd) {
537                        failures.push(format!(
538                            "{prefix}: max_positional={max} but accepted {} positional args",
539                            max + 1,
540                        ));
541                    }
542                }
543            });
544        }
545
546        assert!(failures.is_empty(), "max_positional issues:\n{}", failures.join("\n"));
547    }
548
549    #[test]
550    fn doc_generation_non_empty() {
551        let mut failures = Vec::new();
552
553        for def in COMMAND_DEFS {
554            let doc = def.to_doc();
555            if doc.description.trim().is_empty() {
556                failures.push(format!("{}: CommandDef produced empty doc", def.name));
557            }
558            if doc.url.is_empty() {
559                failures.push(format!("{}: CommandDef has empty URL", def.name));
560            }
561        }
562
563        let check_flat = |def: &crate::command::FlatDef, failures: &mut Vec<String>| {
564            let doc = def.to_doc();
565            if doc.description.trim().is_empty() && !def.policy.bare {
566                failures.push(format!("{}: FlatDef produced empty doc", def.name));
567            }
568            if doc.url.is_empty() {
569                failures.push(format!("{}: FlatDef has empty URL", def.name));
570            }
571        };
572        for def in coreutils::all_flat_defs()
573            .into_iter()
574            .chain(xcode::xcbeautify_flat_defs())
575        {
576            check_flat(def, &mut failures);
577        }
578        for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()) {
579            check_flat(def, &mut failures);
580        }
581
582        assert!(failures.is_empty(), "doc generation issues:\n{}", failures.join("\n"));
583    }
584
585    #[test]
586    fn registry_covers_handled_commands() {
587        let registry = full_registry();
588        let mut all_cmds: HashSet<&str> = registry
589            .iter()
590            .map(|e| match e {
591                CommandEntry::Positional { cmd }
592                | CommandEntry::Custom { cmd, .. }
593                | CommandEntry::Subcommand { cmd, .. }
594                | CommandEntry::Delegation { cmd } => *cmd,
595            })
596            .collect();
597        for def in COMMAND_DEFS {
598            all_cmds.insert(def.name);
599        }
600        for def in coreutils::all_flat_defs() {
601            all_cmds.insert(def.name);
602        }
603        for def in xcode::xcbeautify_flat_defs() {
604            all_cmds.insert(def.name);
605        }
606        for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()) {
607            all_cmds.insert(def.name);
608        }
609        let handled: HashSet<&str> = HANDLED_CMDS.iter().copied().collect();
610
611        let missing: Vec<_> = handled.difference(&all_cmds).collect();
612        assert!(missing.is_empty(), "not in registry or COMMAND_DEFS: {missing:?}");
613
614        let extra: Vec<_> = all_cmds.difference(&handled).collect();
615        assert!(extra.is_empty(), "not in HANDLED_CMDS: {extra:?}");
616    }
617
618}