basecamp/kamal · error · Kamal::Cli::HookError

Hook `#{hook}` failed:\n#{e.message}

Error message

Hook `#{hook}` failed:\n#{e.message}

What it means

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.

Source

Thrown at lib/kamal/cli/base.rb:251

          hooks_output = KAMAL.config.hooks_output_for(hook)

          # CLI flags override config: -q hides all, -v shows all
          # Config setting :verbose forces output, :quiet forces silence
          hook_verbosity = if KAMAL.verbosity == :info && hooks_output
            VERBOSITY.fetch(hooks_output)
          else
            KAMAL.verbosity
          end

          with_env KAMAL.hook.env(**details, **extra_details) do
            KAMAL.with_verbosity(hook_verbosity) do
              run_locally do
                execute *KAMAL.hook.run(hook)
              end
            end
          rescue SSHKit::Command::Failed => e
            raise HookError.new("Hook `#{hook}` failed:\n#{e.message}")
          end
        end
      end

      def on(*args, &block)
        pre_connect_if_required

        super
      end

      def pre_connect_if_required
        if !KAMAL.connected?
          run_hook "pre-connect", secrets: true unless options[:skip_hooks]
          KAMAL.connected = true
        end
      end

      def command

View on GitHub (pinned to eee0083b38)

Solutions

  1. 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>.
  2. Run the hook manually with the same env Kamal would set: `cd .kamal/hooks && KAMAL_VERSION=x ./pre-deploy` to reproduce locally.
  3. Ensure the script is executable and has a valid shebang: `chmod +x .kamal/hooks/<hook-name>` and `head -1` shows #!/bin/sh or similar.
  4. 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.
  5. Verify any binaries the hook calls exist on the machine running kamal (`which curl jq docker`).

Example fix

# before (.kamal/hooks/pre-deploy)
#!/bin/sh
curl -fsSL https://internal.example/notify | jq .status
# after: tolerate optional endpoint failures
#!/bin/sh
curl -fsSL https://internal.example/notify | jq -e '.status == "ok"' || echo "warn: notify failed, continuing"
Defensive patterns

Strategy: try-catch

Validate before calling

# Validate a hook before relying on it (run where kamal runs):
hook = ".kamal/hooks/pre-deploy"
File.executable?(hook) || abort("#{hook} missing or not executable")
system("#{hook} >/dev/null") || abort("hook exits non-zero; fix before deploy") # dry-run where possible

Type guard

def failing_hook?(hook_name)
  path = File.join(".kamal/hooks", hook_name.to_s)
  !File.exist?(path) || !File.executable?(path)
end

Try / catch

begin
  Kamal::CLI::Deploy.new([].tap { }).call # or invoke via Kamal::Commander
rescue Kamal::HookError => e
  warn "deploy hook failed: #{e.message}"
  # surface hook stderr in CI, decide: abort or continue with `kamal deploy --skip_hooks` style flow
  raise
end

Prevention

When it happens

Trigger: 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.

Common situations: 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).

Related errors


AI-assisted analysis of basecamp/kamal@eee0083b38 (2026-08-21). Data as JSON: /api/errors/962755f0933d8191. Report an issue: GitHub.