{"record":{"id":"962755f0933d8191","repo":"basecamp/kamal","slug":"hook-hook-failed-n-e-message","errorCode":null,"errorMessage":"Hook `#{hook}` failed:\\n#{e.message}","messagePattern":"Hook `#(.+?)` failed:\\\\n#(.+?)","errorType":"exception","errorClass":"Kamal::Cli::HookError","httpStatus":null,"severity":"error","filePath":"lib/kamal/cli/base.rb","lineNumber":251,"sourceCode":"\n          hooks_output = KAMAL.config.hooks_output_for(hook)\n\n          # CLI flags override config: -q hides all, -v shows all\n          # Config setting :verbose forces output, :quiet forces silence\n          hook_verbosity = if KAMAL.verbosity == :info && hooks_output\n            VERBOSITY.fetch(hooks_output)\n          else\n            KAMAL.verbosity\n          end\n\n          with_env KAMAL.hook.env(**details, **extra_details) do\n            KAMAL.with_verbosity(hook_verbosity) do\n              run_locally do\n                execute *KAMAL.hook.run(hook)\n              end\n            end\n          rescue SSHKit::Command::Failed => e\n            raise HookError.new(\"Hook `#{hook}` failed:\\n#{e.message}\")\n          end\n        end\n      end\n\n      def on(*args, &block)\n        pre_connect_if_required\n\n        super\n      end\n\n      def pre_connect_if_required\n        if !KAMAL.connected?\n          run_hook \"pre-connect\", secrets: true unless options[:skip_hooks]\n          KAMAL.connected = true\n        end\n      end\n\n      def command","sourceCodeStart":233,"sourceCodeEnd":269,"githubUrl":"https://github.com/basecamp/kamal/blob/eee0083b38661c3707c6b6052cc89e85038a096c/lib/kamal/cli/base.rb#L233-L269","documentation":"Kamal runs local hook scripts from .kamal/hooks/ (e.g. pre-build, pre-deploy, post-deploy) around many CLI commands, passing details like KAMAL_VERSION and KAMAL_HOSTS as environment variables. When the hook script is executed locally via SSHKit and exits with a non-zero status, SSHKit raises SSHKit::Command::Failed, which Kamal converts into Kamal::HookError with the hook's name and the underlying failure message. The wrapping command (deploy, build, etc.) then aborts, so a broken hook blocks the whole deployment.","triggerScenarios":"Any Kamal command that fires a hook (kamal deploy, kamal build, kamal app start, etc.) when the matching script in .kamal/hooks/<hook-name> exits non-zero: a Ruby/shell script with a raising step, a script referencing a missing file, a non-executable or bad-shebang script, or a hook calling a tool not on the local PATH.","commonSituations":"A pre-deploy hook that curls a health endpoint which is down; hooks written on macOS with env-dependent paths that fail in CI; hook scripts copied from templates without chmod +x; a hook using `set -e` plus a failing grep; hook output polluted by a missing dependency (jq, curl, docker).","solutions":["Read the message after the colon: it contains the hook's stderr/stdout that caused the non-zero exit — fix that root cause in .kamal/hooks/<hook-name>.","Run the hook manually with the same env Kamal would set: `cd .kamal/hooks && KAMAL_VERSION=x ./pre-deploy` to reproduce locally.","Ensure the script is executable and has a valid shebang: `chmod +x .kamal/hooks/<hook-name>` and `head -1` shows #!/bin/sh or similar.","If the hook is optional or flaky, guard its body so transient failures exit 0, or temporarily rename it (e.g. pre-deploy.skip) to unblock a deploy.","Verify any binaries the hook calls exist on the machine running kamal (`which curl jq docker`)."],"exampleFix":"# before (.kamal/hooks/pre-deploy)\n#!/bin/sh\ncurl -fsSL https://internal.example/notify | jq .status\n# after: tolerate optional endpoint failures\n#!/bin/sh\ncurl -fsSL https://internal.example/notify | jq -e '.status == \"ok\"' || echo \"warn: notify failed, continuing\"","handlingStrategy":"try-catch","validationCode":"# Validate a hook before relying on it (run where kamal runs):\nhook = \".kamal/hooks/pre-deploy\"\nFile.executable?(hook) || abort(\"#{hook} missing or not executable\")\nsystem(\"#{hook} >/dev/null\") || abort(\"hook exits non-zero; fix before deploy\") # dry-run where possible","typeGuard":"def failing_hook?(hook_name)\n  path = File.join(\".kamal/hooks\", hook_name.to_s)\n  !File.exist?(path) || !File.executable?(path)\nend","tryCatchPattern":"begin\n  Kamal::CLI::Deploy.new([].tap { }).call # or invoke via Kamal::Commander\nrescue Kamal::HookError => e\n  warn \"deploy hook failed: #{e.message}\"\n  # surface hook stderr in CI, decide: abort or continue with `kamal deploy --skip_hooks` style flow\n  raise\nend","preventionTips":["Keep hooks short, exit 0 on non-fatal problems, and reserve non-zero exits for true blockers.","Test hooks in CI with the same env kamal sets (KAMAL_VERSION, KAMAL_HOSTS, KAMAL_COMMAND).","chmod +x every script in .kamal/hooks and commit a valid shebang in each.","Invoke binaries by absolute path or verify them with `command -v` at the top of the hook."],"tags":["kamal","hooks","deploy","exit-code","sshkit"],"backgroundTag":"deploy-hook-failed","analyzedSha":"eee0083b38661c3707c6b6052cc89e85038a096c","analyzedAt":"2026-08-21T15:17:22.045Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}