Repository navigation
Conversation
The hook runs the first time a command needs the configuration, before it loads. Each NAME=value line it writes to $KAMAL_ENV is set in the environment for the rest of the run, where the config's ERB, the secrets and later hooks can read it, and KAMAL_DESTINATION picks the destination to load. That's enough to pick a free review slot, default the destination, or feed computed values into deploy.yml without ERB that reaches outside the file. It gets KAMAL_DESTINATION, KAMAL_COMMAND, KAMAL_SUBCOMMAND and KAMAL_ENV. A destination left in the caller's environment is cleared, since only -d should reach the hook. There's no configuration yet, so the hook is always read from .kamal/hooks and its output follows -v and -q rather than hooks_output. --hosts, --roles and --primary load the configuration while the CLI is still initializing, before Thor records the invocation, so fall back to the command being started to name it.
Aliases are found by reading the raw config before the command they expand to is configured, so that read has to see the hook's destination and variables too: `kamal console -d beta` would otherwise look for a deploy.beta.yml the hook was about to replace, and config ERB reading a hook variable would fail. The expanded command configures afresh, so the hook runs again for it. The first run has KAMAL_COMMAND set to the alias's name, so a hook that leases a destination can leave that to the second, for the command it knows, rather than leasing one slot to look the alias up and another to deploy.
Any command can run the hook, --raw ones included, and the CLI object it runs from doesn't know about a subcommand's --raw. So run it quietly unless -v, and say when it changes the destination on stderr, keeping `kamal server exec --raw -d beta 'cat backup.tar'` byte-clean. Reject a NUL in a value as an invalid line, before anything is set, rather than letting ENV.update fail halfway through, and read the file as bytes so a value that isn't UTF-8 is set as written instead of raising out of the line match. And only drop the destination resolver once it has succeeded, so a caller that rescues a failed hook and asks for the config again runs it again instead of loading the destination it refused.
--hosts, --roles and --primary load the configuration while Main is still initializing, before Thor has recorded either Main's `app` or App's `boot`, so the hook would get KAMAL_SUBCOMMAND=nil for `kamal app boot --hosts 1.1.1.1`. Take the first argument Main has left once it has parsed its options instead, wherever they were given, and name it as Thor will, so `kamal app --hosts 1.1.1.1 bo` is `boot` too. A pre-configure hook that leases a destination needs this to tell `kamal app boot` from `kamal app logs`.
This was referenced Oct 8, 2026
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
jeremy
added this pull request to stack #1965
October 8, 2026 21:01
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #1782, #1783 and #1785. Stacked on #1963; GitHub retargets this to
mainonce that merges.A
pre-configurehook runs before Kamal loads the configuration. EachNAME=valueline it writes to$KAMAL_ENVis set in Kamal's environment for the rest of the run. That's the same contract as GitHub Actions'$GITHUB_ENV. If one of those lines isKAMAL_DESTINATION, Kamal loads that destination instead of the one given with-d.The motivating case is a pool of shared review destinations (
beta1…beta16, each with its owndeploy.betaN.yml).kamal deploy -d betashould lease a free one and deploy there, andkamal app logs -d betashould go to the one you already hold. That takes something that runs before the config is chosen and knows which command is running. It also covers defaulting a destination, and feeding computed values intodeploy.ymlwithout ERB that reaches outside the file.Why one PR instead of four
#1782, #1783, #1784 and #1785 grew around that one need, and each added a mechanism:
KAMAL_MESSAGEside channel for printing.kamal/binprepended to every hook'sPATHTaken back to first principles, only one of those has to live in Kamal: something must run before the configuration exists and be able to change which configuration loads. Everything else either already exists or belongs to the app. So this PR is that one thing, done carefully: one hook, an env-file for its output, the destination rewrite, and enough context about the command for the hook to decide what to do.
What was dropped, and why:
KAMAL_OUTPUTfor every hook, accumulated across hooks (Let hooks talk back to Kamal via KAMAL_OUTPUT #1782). No use for it turned up beyond picking the destination. Anything the pre-configure hook exports is already in the environment of every later hook, the config's ERB and.kamal/secrets.KAMAL_MESSAGE. It was a second output path that ignoredhooks_output: quietand passed terminal control characters straight through. Kamal now announces a changed destination itself. A hook that wants to talk to the user can already do so withhooks_output: { post-deploy: verbose }(Allow configurable verbosity for hooks output #1758).export FOO=barand ignored malformed lines. The format is now strictlyNAME=valueper line, with blank lines and#comments allowed. Values are taken as written, quotes included, and are never evaluated. Anything else, including a NUL, fails the hook before any variable is set..kamal/binto every hook'sPATH(Prepend .kamal/bin to PATH during hook execution #1785). That changed what every existing hook runs. An executable.kamal/bin/rubywould take over#!/usr/bin/env ruby, and.kamal/bin/gitwould take overgit. A hook can call.kamal/bin/claimsby its path.kamal-*: run external commands from .kamal/bin and the PATH, after aliases #1784) continue separately, as an independent feature. Nothing here depends on them.Behaviour
kamal version,kamal init,kamal docs,kamal secrets fetchandkamal secrets extractnever load it, so they never run the hook.--skip-hooksskips it. It is always.kamal/hooks/pre-configure, sincehooks_pathlives in the configuration that hasn't loaded yet.KAMAL_DESTINATIONis set only from-d. A value inherited from the caller's environment is cleared, so a nestedkamalrun from another hook can't pass its destination along by accident.KAMAL_COMMANDandKAMAL_SUBCOMMANDname the command, e.g.app/boot. This holds even when--hosts,--rolesor--primaryload the configuration while the CLI is still initializing, before Thor has recorded anything, and an abbreviation is named as Thor will run it (kamal app --hosts x boisboot).KAMAL_ENVis the path of a private tempfile.-v, because any command can trigger it, includingkamal server exec --raw, whose stdout has to stay byte-clean. When it fails, its output is in theHookError. A changed destination is announced on stderr:Using destination beta3 from the pre-configure hook.KAMAL_COMMANDset to the alias's name, so a hook with side effects, like taking a lease, can save them for the commands it recognises. The sample hook says so.HookErrorand nothing it wrote is applied. If a caller rescues that and asks for the configuration again, the hook runs again rather than Kamal falling back to the destination it refused. A hook that succeeded is never re-run, even if the configuration then fails to load.Depends on
#1963 (
fix-subcommand-env): onmain,KAMAL_SUBCOMMANDis never set for a subcommand run from the command line, and a hook that should treatkamal app bootdifferently fromkamal app logscan't tell them apart without it. This PR adds the same naming for the case where the hook runs while the CLI is still initializing.Tests
test/cli/pre_configure_test.rbcovers:Mainwith selectors before and after the subcommand, and an abbreviated subcommand--skip-hooks, commands that never load the config, and no hook at alltest/commander_test.rbcovers when the destination resolver runs, and what happens after it fails.mainwithout a Docker builder (CliBuildTest ×4, CommandsBuilderTest ×2, ConfigurationBuilderTest ×1). RuboCop is clean. The Docker integration suite wasn't run.kamal app boot -d betaleasesbeta1and deploys there, a laterkamal config -d betaloadsbeta1without leasing, andkamal server exec --raw -d betakeeps the announcement off stdout.