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 the

View on GitHub (pinned to 7fd6ef3c07)

Solutions

  1. Read the wrapped cause (errors.Unwrap or stderr output) to identify the exact panic value and stack origin inside the plugin.
  2. Update the plugin to a version compatible with the current CLI, or fix the panic site if you are the plugin author.
  3. Remove or disable the offending plugin from hook configuration if it is not needed.
  4. 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

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


AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04). Data as JSON: /api/errors/45d2913557c9ea01. Report an issue: GitHub.