Getting started¶
Splashdown pins per-checkout system resources (dev ports, env vars, and mobile simulators or emulators) and coordinates them across every git checkout on your machine, so two worktrees of the same project never collide. This page walks through a first setup for a web or backend project. For a mobile app, see Getting started with mobile.
Prerequisites¶
- Git.
- Recommended: a shell env loader (mise, direnv, or devbox). Splashdown writes a
splashdown.envfile and wires your loader to source it automatically when youcdinto the project. mise is the smoothest option. A loader is not strictly required. Without one you can source the file yourself (set -a; . ./splashdown.env; set +a), or send the values straight into an app.envfile (see Writing straight to a .env file).
Install¶
This puts splash on your PATH. The machine-wide registry lives at $XDG_STATE_HOME/splashdown/ (default ~/.local/state/splashdown/) and is shared across every repo.
Tab-completion is optional but recommended:
# zsh (~/.zshrc)
eval "$(splash completion zsh)"
# bash (~/.bashrc)
eval "$(splash completion bash)"
More detail at Shell completion.
Initialize a project¶
Run this once at the repo root:
Init creates configuration in the current directory. Use splash --cwd PATH init to choose
another directory. A subdirectory of a Git worktree can be an independent Splashdown project.
An existing recipe requires --overwrite to replace it.
Nested init leaves the worktree-root post-checkout hook untouched because Git invokes that hook
from the root. Init prints the splash --cwd PATH sync command to run manually after checkout.
Splashdown scans the filesystem, detects your workspace layout and framework, and does four things:
- Writes
splashdown.toml, the committed recipe describing this project's per-checkout resources. - Writes
splashdown.local.toml, a per-checkout file seeded with commented examples, and makes sure Git ignores it. - Wires your env loader (
mise.toml,.envrc, ordevbox.json) to sourcesplashdown.env, and writes the project's own post-checkout hook configuration for Husky, Lefthook, pre-commit, prek, or simple-git-hooks. Overcommit and a customcore.hooksPathare left untouched with manual forwarding instructions. - Adds managed framework and port guidance to an existing root
AGENTS.mdor independentCLAUDE.md, unless another tool generates that file.
Init asks Git which of those files are already ignored and adds only the rules that are missing,
inside a marked block at the end of .gitignore. An existing rule of your own such as *.env
counts, so nothing redundant is added, and your other lines and comments are never touched. If you
point --env-file at another file, that file gets the rule and splashdown.env does not.
Init writes configuration only. It allocates nothing, records no trust, and installs nothing on your machine, so you can read and edit the generated recipe before anything else happens.
Sample output for a small backend:
scanning project…
detected: single (main)
. → node-backend
shell loader → mise (detected mise.toml)
wrote splashdown.toml
wrote splashdown.local.toml (skeleton)
updated .gitignore (+/splashdown.local.toml, +/splashdown.env)
updated mise.toml (+_.file = "splashdown.env")
note: the local post-checkout hook is installed by `splash trust`
changed: splashdown.toml, splashdown.local.toml, mise.toml
configuration written; nothing is allocated or active yet
next: run `splash trust` to activate automatic post-checkout handling
run `splash` to allocate values and write splashdown.env
Commit the recipe
Commit splashdown.toml and the loader change. Do not commit splashdown.local.toml or splashdown.env (both are gitignored by init). Committing the recipe is what lets new worktrees inherit it.
Activate this checkout¶
splash trust authorizes automatic handling for this clone. It installs the local post-checkout
hook, runs your hook manager's own install command when one of them owns the event, and runs
mise trust or direnv allow when the loader file holds splashdown's integration and nothing
else. A loader
file that also holds settings of your own is left for you to approve with the loader's own
command.
Bare splash is a sync. It allocates this checkout's resources and writes splashdown.env:
Every sync that writes a file also checks its destinations against Git and names any that would
still show up in git status:
That is a read, never an edit, because sync runs from the post-checkout hook and must not dirty a
fresh clone. The note matters most on a clone or a worktree that is not the one where init ran.
Init reuses ignore rules that are already effective, and a rule in your personal
core.excludesFile or in this clone's .git/info/exclude counts. Neither is committed, so a
destination your own setup happens to hide can reach the repository with no rule protecting it, and
this note on a teammate's first sync is the only thing that says so. Add the rule to .gitignore
and commit it.
What got created¶
| File | Committed | Purpose |
|---|---|---|
splashdown.toml |
Yes | The recipe: resources, apps, and (for mobile) device targets |
splashdown.local.toml |
No | Per-checkout additions, seeded with commented examples (gitignored) |
splashdown.env |
No | Generated KEY=VALUE file: splashdown rewrites the keys your recipe declares and leaves anything else in it alone (gitignored) |
| loader config | Yes | Gains one line that sources splashdown.env, and is created if absent |
AGENTS.md / CLAUDE.md |
Yes | Existing files gain a sentinel-wrapped block telling coding agents which port variables, env destination, and launch commands to use |
Splashdown never creates an agent-instruction file. If both exist and CLAUDE.md imports
@AGENTS.md, only AGENTS.md keeps guidance, and any older complete block in CLAUDE.md is
removed. init --overwrite replaces or removes the managed block as detected frameworks change,
and deinit removes the block while leaving the rest of each Markdown file untouched. Symlinks,
non-regular files, and unpaired or duplicate sentinels are treated as user-owned: Splashdown
warns and leaves them unchanged.
The block names the env destination your recipe configures, so a project pointed at .env or
config/dev.env reads about that file rather than splashdown.env. It carries no allocated
values, which keeps it safe to commit. If the file is generated by another tool, Splashdown
prints the generator marker it found, prints the block itself, and asks you to copy it into
that file's source instead of editing output that would be overwritten.
See How it works for the full model.
Verify it¶
Open a new shell in the project (so the loader re-reads its config), then check the value is present:
splash status # resource keys and port state (values stay hidden)
splash --show-values status # include values when you need to inspect them
echo $PORT # e.g. 9081, loaded by your env loader
If $PORT is empty, your loader has not picked up splashdown.env yet. Run splash doctor to check the wiring, and confirm your loader is active in this directory.
The payoff: worktrees get their own ports¶
Add a second checkout and the post-checkout hook provisions it automatically, with no manual editing:
git worktree add ../myapp.feature feature
cd ../myapp.feature
splash --show-values status # PORT is 9082 here, not 9081
Both checkouts can run their dev servers at once without a port clash. The machine-wide registry guarantees it, even across unrelated repos.
If the project also declares trusted one-time setup under [bootstrap], a trusted clone runs it
after provisioning a newly-created worktree. Fresh clones use splash trust followed by
splash bootstrap. See Trusted worktree bootstrap.
mise and direnv treat the inherited loader file at the new path as untrusted. Splashdown does
not approve project-controlled files from a hook, so review the file and run mise trust or
direnv allow in the new worktree if your loader asks.
Editing the recipe¶
Open splashdown.toml to add or change resources. A port and a templated value, for example:
[resources.PORT]
type = "port"
range = [9081, 9100] # globally-coordinated lowest-free port in this range
[resources.DATABASE_URL]
type = "template"
template = "postgres://localhost/myapp_{{ slug(branch) }}"
Re-run splash (or just switch branches) to apply. The full schema is in The recipe.
Splashdown validates the entire recipe before it allocates anything or updates generated files. Unknown sections or fields, mistyped values, invalid templates, and bad references stop the sync with a qualified error such as [resources.PORT.range], so a late typo cannot leave a partially applied configuration.
Writing straight to a .env file¶
If an app already reads its own .env, choose that file as the destination when you initialize:
That records env_file = ".env" under [project] and every resource without its own writer
lands there, so the app keeps its ordinary launch command. Pick a loader as well and the loader is
wired to read that same file, which is the option to take when your shell needs the values too.
Splashdown manages only the keys your recipe declares. Other lines, comments, and blank lines stay
where they are, and an existing value for a declared key is replaced on the next sync. A key the
file assigns twice, or assigns in a shape splashdown does not rewrite such as PORT: 3000, is
reported as an error instead of being joined by a second definition.
The path is relative to the checkout root, for example --env-file apps/web/.env in a monorepo.
Missing parent directories are created. Absolute paths and paths containing .. are rejected
before init writes anything.
To send one resource somewhere other than the default, give it a writer of its own:
[resources.LEGACY_PORT]
type = "port"
range = [9999, 10100]
writer = "envfile=path/to/legacy/.env" # writes `LEGACY_PORT=9999` there
That resource is written only to its own destination and is not copied into the default file, so a loader will not see it. Prefer the default destination when both an app and your shell need the value.
Keeping wiring healthy¶
splash doctorchecks that your loader and the git hook are wired, and that no config file hardcodes a port over the env var.splash doctor --fixre-applies safe fixes.splash syncforces a re-provision.splash gcdrops registry entries for checkouts you have since deleted.
Next steps¶
- The recipe: resources, templates, and the full schema.
- Per-checkout overrides: add variants in
splashdown.local.toml, and machine-wide devices. - Monorepos: multi-app workspaces, worked end to end.
- Getting started with mobile: simulators, emulators, and physical devices.