{"record":{"id":"45d2913557c9ea01","repo":"larksuite/cli","slug":"hook-q-panic-w","errorCode":null,"errorMessage":"hook %q panic: %w","messagePattern":"hook %q panic: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/hook/install.go","lineNumber":263,"sourceCode":"// **Factory-local state lifetime contract**: any value the plugin's\n// outer factory captures (`state` in the example above) is now created\n// PER INVOCATION of the wrapped command -- it is NOT a one-shot init\n// the way Plugin.Install is. Plugins that need long-lived state (a\n// connection pool, an LRU cache, a metrics counter) MUST hold it on\n// the Plugin struct or in a package-level variable; relying on\n// closure-local memoisation inside the wrapper factory will silently\n// reset on every command dispatch.\nfunc recoverWrap(fullName string, w platform.Wrapper) platform.Wrapper {\n\treturn func(next platform.Handler) platform.Handler {\n\t\treturn func(ctx context.Context, inv platform.Invocation) (returned error) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t// Preserve the panic value's error identity in the cause\n\t\t\t\t\t// chain when it is an error, so errors.Is/As can still reach\n\t\t\t\t\t// it; fall back to %v formatting for non-error panics.\n\t\t\t\t\tcause := fmt.Errorf(\"hook %q panic: %v\", fullName, r)\n\t\t\t\t\tif e, ok := r.(error); ok {\n\t\t\t\t\t\tcause = fmt.Errorf(\"hook %q panic: %w\", fullName, e)\n\t\t\t\t\t}\n\t\t\t\t\treturned = errs.NewValidationError(errs.SubtypeFailedPrecondition,\n\t\t\t\t\t\t\"hook %q panicked: %v\", fullName, r).\n\t\t\t\t\t\tWithHint(\"plugin hook %q crashed while handling this command; report the panic to the plugin author or remove the plugin\", fullName).\n\t\t\t\t\t\tWithCause(cause)\n\t\t\t\t}\n\t\t\t}()\n\t\t\t// Construct AFTER the recover is armed so a panicking\n\t\t\t// factory becomes a hook envelope instead of a process\n\t\t\t// crash.\n\t\t\tinner := w(next)\n\t\t\treturn inner(ctx, inv)\n\t\t}\n\t}\n}\n\n// namespacedWrap wraps a plugin's Wrapper so any *platform.AbortError it\n// returns is replaced with a fresh copy whose HookName is the","sourceCodeStart":245,"sourceCodeEnd":281,"githubUrl":"https://github.com/larksuite/cli/blob/7fd6ef3c07182257ce776cdc5a614e122d5bd4b3/internal/hook/install.go#L245-L281","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: hook panics on nil config\nfunc OnCommand(ctx context.Context, cfg *Config) error {\n    return cfg.Name // panics: nil deref\n}\n\n// after\ntype Config struct{ Name string }\nfunc OnCommand(ctx context.Context, cfg *Config) error {\n    if cfg == nil {\n        return errors.New(\"config is required\")\n    }\n    _ = cfg.Name\n    return nil\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"var vErr *errs.ValidationError\nif errors.As(err, &vErr) && vErr.Subtype == errs.SubtypeFailedPrecondition {\n    var panicErr error\n    if errors.As(err, &panicErr) {\n        log.Printf(\"plugin panicked: %v\", panicErr)\n    }\n}","tryCatchPattern":"if err := cmd.Run(ctx); err != nil {\n    var vErr *errs.ValidationError\n    if errors.As(err, &vErr) && strings.Contains(vErr.Error(), \"panicked\") {\n        fmt.Fprintf(os.Stderr, \"plugin hook crashed: %v\\nhint: %s\\n\", err, vErr.Hint)\n        os.Exit(1)\n    }\n    return err\n}","preventionTips":["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."],"tags":["plugin","hook","panic","validation-error"],"backgroundTag":"plugin-hook-panic","analyzedSha":"7fd6ef3c07182257ce776cdc5a614e122d5bd4b3","analyzedAt":"2026-09-04T21:17:44.649Z","contentChangedAt":"2026-09-04T21:17:44.649Z","schemaVersion":2},"datasetVersion":"2026-09-12T02:17:10.037Z"}