bight
Patches .env files automatically on git checkout — keeping local environments in sync with the current branch.
git checkout feat-login
# bight: .env → DB_NAME=myapp_feat-login
# bight: .env → JWT_SECRET=***
Only the listed vars are touched. The rest of your .env is left untouched.
Installation
With Homebrew:
brew install andrewadev/tap/bight
With Go:
go install github.com/AndrewADev/bight@latest
From a release binary:
Download the binary for your platform from the releases page:
| Platform |
File |
| macOS (Apple Silicon) |
bight-darwin-arm64 |
| macOS (Intel) |
bight-darwin-amd64 |
| Linux (x86-64) |
bight-linux-amd64 |
| Linux (ARM64) |
bight-linux-arm64 |
| Windows |
bight-windows-amd64.exe |
Then make it executable and put it on your PATH:
chmod +x bight-darwin-arm64
mv bight-darwin-arm64 /usr/local/bin/bight
Each release includes a checksums.txt for verification:
sha256sum -c checksums.txt --ignore-missing
Trying a PR preview:
Once a maintainer has triggered the Preview workflow on a PR (see DEVELOPING.md), per-platform binaries plus a checksums.txt are attached to the run as artifacts, downloadable by anyone with read access. Preview binaries report a version like v0.0.0-preview-pr<N>-<sha> so they can't be mistaken for a release.
If you have the Task runner and gh CLI installed, you can grab the right one for your platform with:
task preview-fetch -- 42 # downloads the artifact for your OS/arch into dist/preview-pr42/
It prints the install command you can run to drop the binary onto your PATH.
Getting started
1. Install
Run once per repo, after cloning:
bight install
This writes the git hook and walks you through creating a .bight.yml config:
bight: hook installed
bight: no config file found. Create .bight.yml? [Y/n]
Project name [myapp]:
Env file path [.env]:
Add env vars to track? [Y/n]
(blank name to finish)
Var name: DB_NAME
Strategy:
1) template - interpolate branch/project name (default)
2) random - fresh random value on each checkout
Choice [1]: 1
Var name:
bight: created .bight.yml
2. Confirm everything is wired up
bight doctor
bight doctor:
[ok] git repo detected
[ok] config: .bight.yml loaded
[ok] config: project = "myapp", 1 env file(s)
[ok] hook: installed
[ok] env file: .env
[ok] vars: all strategies valid
[ok] template: "myapp_main" for branch "main" (vars: [DB_NAME])
[ok] vars: all triggers valid
3. Preview before it fires automatically
bight run --dry-run
# bight (dry-run): .env → DB_NAME=myapp_main
No files are touched. When you're happy with what you see, you're done — the hook fires on every checkout from here on.
4. Switch branches
git checkout -b feat-login
# bight: .env → DB_NAME=myapp_feat-login
Reference
Commands
Full per-command reference (flags, usage, subcommand tree) is auto-generated from the CLI itself and lives in docs/commands/bight.md. You can also run bight help <command> locally.
Config file
bight install generates a starter config, but you can hand-edit .bight.yml at any time:
project: myapp
defaults:
branch_template: "{{.Project}}_{{.Branch}}" # used by the template strategy
env_files:
- path: .env
backup: true # write .env.bak before patching (optional, default false)
vars:
- name: DB_NAME
strategy: template # renders to e.g. myapp_feat-login
on: checkout
- name: JWT_SECRET
strategy: random # fresh 64-char hex string on every branch switch
on: checkout
sensitive: true # mask value in console output
Using a non-default config file
If your config isn't named .bight.yml or lives at a non-standard path, use --config:
bight run --config path/to/custom.bight.yml
bight doctor --config path/to/custom.bight.yml
--config is a global flag — it works with any subcommand that reads config.
Alternatively, set the BIGHT_CONFIG environment variable. This is convenient for the post-checkout hook itself or CI jobs where passing a flag is awkward:
export BIGHT_CONFIG=path/to/custom.bight.yml
bight doctor # picks up BIGHT_CONFIG automatically
Precedence: --config > BIGHT_CONFIG > auto-discovery of .bight.yml in the current directory. If BIGHT_CONFIG points to a missing or unreadable file, bight will error rather than silently falling back to auto-discovery. bight doctor reports which source was used.
Manual patching
To apply env patching for the current branch without switching:
bight run
Tip: to test how another branch would be patched, suppress the hook when switching so bight doesn't fire automatically, then use --dry-run:
git -c core.hooksPath=/dev/null checkout other-branch
bight run --dry-run
Strategies
| Strategy |
Output |
Typical use |
template |
Rendered from {{.Project}} / {{.Branch}} |
DB_NAME |
random |
Fresh 32-byte hex string |
JWT_SECRET, tokens |
deterministic |
Stable 64-char hex derived from project + branch |
DB_NAME (same value across machines) |
Template functions
branch_template is a Go text/template and supports these functions:
| Function |
Effect |
Example |
slug |
Lowercases, transliterates accented Latin letters to ASCII (é->e, ß->ss, ø->o), and collapses each run of other characters outside a-z0-9 into one _, trimming leading/trailing _ |
{{.Branch | slug}}: feat/Crème-Brûlée -> feat_creme_brulee |
slug produces names that are valid unquoted identifiers in most databases:
defaults:
branch_template: "{{.Project}}_{{.Branch | slug}}" # myapp_feat/login-page -> myapp_feat_login_page
Sensitive vars (sensitive)
Mark a var sensitive: true to prevent its value from appearing in console output. The value is still written to the .env file normally — only the terminal display is affected.
- name: JWT_SECRET
strategy: random
on: checkout
sensitive: true
Output with sensitive: true:
bight: .env → JWT_SECRET=***
Backup files (backup)
Set backup: true on an env file entry to write a copy of the file to {path}.bak before each patch is applied. Useful for inspecting what changed or recovering a previous value.
env_files:
- path: .env
backup: true
vars:
- name: DB_NAME
strategy: template
on: checkout
The backup is a verbatim copy of the file as it was immediately before patching. It is overwritten on each checkout — only the most recent pre-patch state is kept.
godotenv strips comments and sorts keys on rewrite. defaults.collect-comments collects comments before the patch is applied and writes them back afterwards:
| Comment position before patching |
Position after patching |
Directly above a KEY=… line |
Directly above that key, wherever it sorts to |
Directly below a KEY=… line, followed by a blank line or end of file |
Directly below that key, wherever it sorts to |
| Before the first key, followed by one or more blank lines |
Top of the file |
| Anywhere else |
End of the file |
Note: This is a best-effort feature. Inline comments (KEY=val # note) are lost, and blank lines between keys are not preserved.
| Value |
Behavior |
all |
Preserves every full-line comment |
blocks-only |
Preserves only contiguous comment blocks (≥ 2 lines) — skips isolated # notes |
unset / none |
Comments are not preserved (default) |
defaults:
collect-comments: blocks-only
How values are written
Patching rewrites the whole env file: keys are sorted, and every value, patched or not, is re-encoded. Each value is written in the first of these forms that godotenv reads back unchanged:
| # |
Form |
Chosen for |
Example |
| 1 |
Bare integer |
Integers |
PORT=8080 |
| 2 |
Double quotes |
Most values: no $, not ending in " or \ |
PASSWORD="abc999!" |
| 3 |
Single quotes |
Values with $ or ending in ", on one line, with no ', not ending in \ |
PRICE='$5' |
| 4 |
Double quotes, $ escaped |
Values with $ and a ' or line break |
NOTE="it's \$5" |
| 5 |
Unquoted, $ escaped |
Values ending in \, or ending in " and containing ' |
DIR=C:\My Dir\ |
The order follows these rules:
$ stays unescaped where possible. Inside double quotes, $ starts a variable reference, so a literal $ has to be written as \$. Inside single quotes, $ is literal and needs no escape. Values containing $ are therefore single-quoted when they can be. godotenv reads \$ back as $, but some other .env readers keep the backslash.
- Every value is written on one line. Line breaks and carriage returns are written as
\n and \r inside double quotes, never raw.
A few values fit none of the forms, such as a value with a line break that ends in " or \. For these, patching fails with an error and leaves the file unchanged.
Triggers (on)
| Value |
When |
checkout |
Every branch switch |
Seeding env files (copy)
When you git worktree add a new working tree, you land in a clean directory with no .env. Set copy on an env file to seed it from somewhere — typically the same file in the main worktree:
env_files:
- path: .env
copy: ../main/.env # short form: just the source path
vars: # Optional. Can be omitted when no branch-dependent variable updates desired.
- name: JWT_SECRET
strategy: random
on: checkout
Source paths may be:
- absolute (
/path/to/.env)
~-prefixed (~/envs/myapp.env)
- relative — resolved against the main worktree root, not the current working directory. This is what makes
../main/.env work the same from every linked worktree.
If you need to control overwrite behavior, use the mapping form:
env_files:
- path: .env
copy:
source: ../main/.env
overwrite: true # clobber an existing .env on init
overwrite behavior:
false (default) — if .env already exists, the copy is silently skipped. copy: is a "seed if absent" declaration, not a per-checkout request — once the file exists, bight leaves it alone. To see whether bight thinks it would copy, use bight doctor or bight run --dry-run.
true — if .env already exists it is replaced (after the backup step, if backup: true).
overwrite controls only the file copy. Var patching always rewrites the keys it targets regardless of this setting.
Global config (~/.bight.yml)
Settings in ~/.bight.yml apply across all repos and are overridden field-by-field by the repo's .bight.yml. Only defaults fields are supported globally — env_files and vars must be defined in the repo config. If a repo has no .bight.yml, bight does nothing — the global config alone is not enough to trigger patching.
defaults:
branch_template: "{{.Project}}_{{.Branch}}"
collect-comments: blocks-only
Developing
See DEVELOPING.md