gitbutlerapp/gitbutler · error · anyhow::Error

executable must be specified for non built-ins

Error message

executable must be specified for non built-ins

What it means

Same conversion path (UserDefinedProgramSpec::try_into_program_spec): when the spec does NOT override a builtin (its id matches no PROGRAMS entry), an `executable` value is required so but-api knows what to launch. Its absence bails. The subtle trap: entries intended as builtin overrides that miss the builtin id (typo, renamed builtin id across versions) silently fall into this 'new program' branch and then fail for the missing executable instead of overriding.

Source

Thrown at crates/but-api/src/open/program.rs:745

                    .map(Into::into)
                    .unwrap_or_else(|| builtin.executable.clone()),
                category: self
                    .category
                    .map(Into::into)
                    .unwrap_or_else(|| builtin.category.clone()),
                cli_arg_supplier,
                extensions,
            })
        } else {
            let (name, id) = match (self.name, self.id) {
                (Some(name), Some(id)) => (name, id),
                (Some(name), None) => (name.clone(), name),
                (None, Some(id)) => (id.clone(), id),
                (None, None) => anyhow::bail!("id or name must be specified"),
            };

            let Some(executable) = self.executable else {
                anyhow::bail!("executable must be specified for non built-ins")
            };

            Ok(ProgramSpec {
                id,
                name,
                executable: executable.into(),
                category: self.category.map(Into::into).unwrap_or_default(),
                cli_arg_supplier: CliArgumentSupplier::Custom(CustomCliArgumentSupplier {
                    open_args: self.open_args,
                    open_at_line_args: self.open_at_line_args,
                }),
                extensions,
            })
        }
    }
}

/// The executable to invoke for a program.

View on GitHub (pinned to caf1f223d3)

Solutions

  1. If you meant a brand-new program: add the "executable" block (pathExecutable or macOS bundle identifier)
  2. If you meant to override a builtin: fix the "id" to match the builtin id exactly (check PROGRAMS in crates/but-api/src/open/program.rs); overrides inherit the builtin's executable
  3. Otherwise delete the broken entry - it is silently filtered out of program listings anyway

Example fix

// before: intent was overriding VS Code, but id is wrong -> treated as new program, no executable
{ "id": "code", "openArgs": ["-r", "{filepath}"] }

// after: correct builtin id -> executable inherited from the builtin
{ "id": "vscode", "openArgs": ["-r", "{filepath}"] }
Defensive patterns

Strategy: validation

Validate before calling

// Overrides must match a builtin id exactly; everything else needs an executable
const BUILTIN_IDS = new Set(['vscode', 'intellij', 'zed' /* mirror PROGRAMS in but-api */]);
function isCompleteEntry(e: ProgramEntry): boolean {
  const isOverride = typeof e.id === 'string' && BUILTIN_IDS.has(e.id);
  return isOverride || e.executable !== undefined;
}

Type guard

function isExecutableSpec(x: unknown): x is { type: 'pathExecutable'; path: string } | { type: 'macosApplication'; bundleIdentifier: string } {
  if (typeof x !== 'object' || x === null) return false;
  const t = x as Record<string, unknown>;
  return t.type === 'pathExecutable' && typeof t.path === 'string'
      || t.type === 'macosApplication' && typeof t.bundleIdentifier === 'string';
}

Prevention

When it happens

Trigger: A spec with name/id but no "executable" object; or an override entry whose id does not exactly match a builtin id (e.g. "code" vs "vscode", trailing whitespace, renamed across versions), so the builtin-merge branch is skipped and executable becomes mandatory.

Common situations: Hand-written override that only sets openArgs/openAtLineArgs; GitButler version change renaming a builtin program id so an old override stops matching; partially synced or truncated JSON.

Understand the failure class

Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/889a80395bc98dc4. Report an issue: GitHub.