Dotenv Phase Placement
Every user-facing shell addition lands in a specific phase file inside a specific directory under dotenv/. Pick wrong
and either the thing doesn't load, or it loads at the wrong time, or it leaks across platforms. This skill is the
decision tree.
The seven phase files
In every dotenv/ directory (including platform subdirs) up to seven phase files load, always in this order:
exports → functions → aliases → extra → env → completion → prompt
| Phase file | What goes here |
|---|---|
exports.sh |
export FOO=bar lines, PATH additions, anything that sets environment. |
functions.sh |
Shell function definitions (function foo() { … }). |
aliases.sh |
alias ll='ls -la' lines. |
extra.sh |
Miscellaneous setup that doesn't fit another phase. Use sparingly. |
env.sh |
Runtime environment mutations that must happen after aliases/functions exist. |
completion.sh |
Completion loading / complete -F setup for shell commands. |
prompt.sh |
PS1 / prompt segment configuration. |
Only create the files you actually need. lib/load.sh no-ops missing ones.
The platform chain
Directories load in this order, each adding to (not replacing) the previous layer:
dotenv/ (always)
dotenv/darwin/ (macOS only)
dotenv/linux/ (Linux only)
dotenv/wsl/ (WSL - loaded on top of linux/)
dotenv/wsl2/ (WSL 2 - loaded on top of wsl/)
dotenv/tmux/ (only inside tmux)
dotenv/screen/ (only inside GNU screen)
dotenv/ssh/ (only inside an SSH session)
Rule: if the addition is platform-specific, put it in the platform subdir's phase file - never in a shared file guarded
with if [[ "$(uname)" == Darwin ]]. The platform chain already does that dispatch.
Decision tree
Answer in order; first match wins.
- Is it a standalone executable you'd invoke as
foo args? → Not a phase file. Scaffold abin/script; use thebin-script-scaffoldskill. - Is it a private loader internal (
__dot_*/internal::*)? →dotenv/lib/(or the neighboringlib/under a platform dir). Not a phase file. - Does it only make sense on one platform? → Phase file under the matching platform subdir (
dotenv/darwin/,dotenv/linux/,dotenv/wsl/,dotenv/wsl2/). Same phase-file name rules below. - Does it only matter inside an SSH / tmux / screen session? → Phase file under
dotenv/ssh/,dotenv/tmux/, ordotenv/screen/. - Does it set an env var or
PATH? →exports.shin the appropriate directory. - Is it a function definition? →
functions.sh. - Is it an alias (
alias x='...')? →aliases.sh. - Is it a
complete -F/complete -Wregistration or a completion script source? →completion.sh. - Does it touch the prompt or
PS1? →prompt.sh. - Does it need to run after aliases/functions exist (e.g. calls a function defined earlier)? →
env.sh. - None of the above fit cleanly? →
extra.sh, and leave a one-line comment explaining why it doesn't fit elsewhere.
Common placements
| Addition | File |
|---|---|
export EDITOR=vim |
dotenv/exports.sh |
alias ll='ls -la' |
dotenv/aliases.sh |
function git-recent() { … } |
dotenv/functions.sh |
complete -F _dot_mything mything |
dotenv/completion.sh |
macOS-only export HOMEBREW_NO_ANALYTICS=1 |
dotenv/darwin/exports.sh |
Linux-only alias open='xdg-open' |
dotenv/linux/aliases.sh |
PATH addition for /mnt/c/... on WSL |
dotenv/wsl/exports.sh |
SSH-only function forward-port() { … } |
dotenv/ssh/functions.sh |
A git-sync command-line tool |
dotenv/bin/git-sync (NOT a phase) |
Private helper __dot_prompt_color_for_host |
dotenv/lib/prompt-colors.sh (NOT a phase) |
Mirror the test path
When phase-file additions are non-trivial (a new shell function with logic, a completion registration, a prompt hook),
the matching test lives under tests/<same path>:
dotenv/darwin/aliases.sh→tests/dotenv/darwin/aliases.batsdotenv/functions.sh→tests/dotenv/functions.bats
Do NOT create or extend a bats test file for a trivial one-line addition (a plain alias, a single export VAR=val,
or a simple platform path tweak). The existing phase-file tests already cover load semantics; adding a new @test block
per alias bloats the suite without catching anything. Add behavior tests when the addition has logic worth exercising.
See the bats-test-conventions skill for when and how.
Anti-patterns
- Runtime
unamecheck in a shared file. Move to a platform subdir; that's what the subdir chain is for. - Cross-phase dependencies. Defining an alias in
functions.shor vice versa - it'll load at the wrong time. Respect the phase order. - New phase file names.
lib/load.shhard-codes the seven listed above; a made-up name silently doesn't load. Adding a new phase requires coordinated changes tolib/load.sh- ask first. exportinsidefunctions.sh. Belongs inexports.shso it's available before functions are sourced.- Forgetting a platform chain means WSL gets
linux/ANDwsl/. If the addition is WSL-only, put it indotenv/wsl/, not duplicated underdotenv/linux/.
Update the public surface
After adding or renaming anything user-facing in a phase file, follow the doc-sync skill: update REFERENCE.md
(aliases, functions, env vars, hooks all go in their named tables) and, for new DOT_* / BASHRC_* knobs, update
README.md's Configuration Options table in lockstep.