larksuite/cli · error
hook %q panic: %w
Error message
hook %q panic: %w
What it means
This validation error reports that a plugin hook (e.g. a lifecycle or pre/post command hook) panicked while being invoked by the CLI. The recovered panic value is preserved in the cause chain when it is an error (via %w) so errors.Is/errors.As still work, and a typed errs ValidationError with subtype FailedPrecondition carries a hint directing the user to the plugin author. It exists so a crashing third-party plugin fails loudly and diagnosably instead of silently corrupting command execution.
Source
Thrown at internal/hook/install.go:263
// **Factory-local state lifetime contract**: any value the plugin's
// outer factory captures (`state` in the example above) is now created
// PER INVOCATION of the wrapped command -- it is NOT a one-shot init
// the way Plugin.Install is. Plugins that need long-lived state (a
// connection pool, an LRU cache, a metrics counter) MUST hold it on
// the Plugin struct or in a package-level variable; relying on
// closure-local memoisation inside the wrapper factory will silently
// reset on every command dispatch.
func recoverWrap(fullName string, w platform.Wrapper) platform.Wrapper {
return func(next platform.Handler) platform.Handler {
return func(ctx context.Context, inv platform.Invocation) (returned error) {
defer func() {
if r := recover(); r != nil {
// Preserve the panic value's error identity in the cause
// chain when it is an error, so errors.Is/As can still reach
// it; fall back to %v formatting for non-error panics.
cause := fmt.Errorf("hook %q panic: %v", fullName, r)
if e, ok := r.(error); ok {
cause = fmt.Errorf("hook %q panic: %w", fullName, e)
}
returned = errs.NewValidationError(errs.SubtypeFailedPrecondition,
"hook %q panicked: %v", fullName, r).
WithHint("plugin hook %q crashed while handling this command; report the panic to the plugin author or remove the plugin", fullName).
WithCause(cause)
}
}()
// Construct AFTER the recover is armed so a panicking
// factory becomes a hook envelope instead of a process
// crash.
inner := w(next)
return inner(ctx, inv)
}
}
}
// namespacedWrap wraps a plugin's Wrapper so any *platform.AbortError it
// returns is replaced with a fresh copy whose HookName is theView on GitHub (pinned to 7fd6ef3c07)
Solutions
- Read the wrapped cause (errors.Unwrap or stderr output) to identify the exact panic value and stack origin inside the plugin.
- Update the plugin to a version compatible with the current CLI, or fix the panic site if you are the plugin author.
- Remove or disable the offending plugin from hook configuration if it is not needed.
- Report the panic to the plugin maintainer with the panic message from the hint.
Example fix
// before: hook panics on nil config
func OnCommand(ctx context.Context, cfg *Config) error {
return cfg.Name // panics: nil deref
}
// after
type Config struct{ Name string }
func OnCommand(ctx context.Context, cfg *Config) error {
if cfg == nil {
return errors.New("config is required")
}
_ = cfg.Name
return nil
} Defensive patterns
Strategy: type-guard
Type guard
var vErr *errs.ValidationError
if errors.As(err, &vErr) && vErr.Subtype == errs.SubtypeFailedPrecondition {
var panicErr error
if errors.As(err, &panicErr) {
log.Printf("plugin panicked: %v", panicErr)
}
} Try / catch
if err := cmd.Run(ctx); err != nil {
var vErr *errs.ValidationError
if errors.As(err, &vErr) && strings.Contains(vErr.Error(), "panicked") {
fmt.Fprintf(os.Stderr, "plugin hook crashed: %v\nhint: %s\n", err, vErr.Hint)
os.Exit(1)
}
return err
} Prevention
- Test plugin hooks against nil/empty inputs before shipping.
- Recover defensively inside your own hook entry point.
- Pin plugin versions compatible with the CLI SDK you build against.
- Run plugin hooks in CI with representative command traffic.
When it happens
Trigger: Any registered plugin hook invoked during a command that calls panic() or panics on nil dereference/out-of-range/etc. The recover() in the hook runner at internal/hook/install.go:263 converts the panic into this error instead of crashing the process.
Common situations: A plugin built against an older CLI API hits a nil map/slice; a hook script or binary returns unexpected output and the wrapper panics; a plugin author ships defensive code that panics on unknown config shapes; a version mismatch between the plugin SDK and CLI runtime.
Related errors
- %s %q: hookName must match ^[a-z0-9][a-z0-9-]*$
- %s %q: hookName already used in this plugin
- build embeds no base skill content
- composed skill tree has an unsatisfied required skill
- invalid skill reference remap
AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04).
Data as JSON: /api/errors/45d2913557c9ea01.
Report an issue: GitHub.