Share defaults and keep your overrides
Grove provides mechanism and leaves policy to you. A team pins a shared standard in one committed file, and you still own the last word on your setup, like a team .editorconfig.
Seven layers
| # | Layer | Path or source | Purpose |
|---|---|---|---|
| 1 | Built-in defaults | shipped with Grove | A workable baseline. |
| 2 | User | ${user_config_dir}/grove/config.json |
Per-user defaults across every repo. |
| 3 | Project | <repo>/.grove/config.json |
Team baseline. Commit this. |
| 4 | Project-local | <repo>/.grove/config.local.json |
Per-machine overrides. Gitignored. |
| 5 | Declared env vars | a fixed name per field, e.g. GROVE_GOTIFY_API_URL |
Deployment values for the few fields that name one. |
| 6 | Env vars | GROVE_<SECTION>__<FIELD>=value |
Quick overrides for a single shell session. |
| 7 | CLI flags | (where applicable) | One-shot overrides. |
- Each later layer overrides the earlier one, and an absent layer is skipped.
- Lists replace wholesale, except
agents, which merges byname. - Every surface resolves each repo's cascade independently, so an agent declared in
.grove/config.jsonis honored the same way from the TUI, CLI, daemon and web dashboard. - Pydantic validates the merged result with
extra="forbid", so a typo in any layer raises loudly.
Workspace defaults put the user first
defaults is the one exception to project last. A workspace form is a private workbench, and the repository may suggest its usual setup but must not replace a preference you made for this machine.
- For
defaultsonly, user sits above both project layers. Environment and CLI overrides stay above user. - The inversion is field by field, so a project value still applies when your user layer leaves it unset.
- Save as defaults in the new workspace modal writes these answers. See new-workspace defaults.
Lists merge by name, only for agents
Grove ships claude, codex and shell. A project layer that adds one entry gets all four.
- New names append, matching names merge field by field, so overriding only
claude'scommandkeepskind: "claude_code"for the Activity Dashboard. - A mutually exclusive pair such as
init_script.inlineandpathoverrides as a group. The highest layer to set either wins both. See init scripts.
Placeholders and environment variables
${repo}and${repo_name}expand at consume time to the repo root and its basename, so one config serves every repo.GROVE_<SECTION>__<FIELD>=valueoverrides a single field for one invocation, double underscore per nesting level.${VAR}in any string value pulls from a variable you name. An unset variable is an error, since writing the reference was deliberate.$${VAR}is the escape.- A few fields declare a fixed variable, such as
GROVE_GOTIFY_API_URL, listed in the configuration reference. An unset one simply does not override. - A secret never goes through any of these. It stays on a
*_envfield that holds only the variable's name.
GROVE_TMUX__HISTORY_LIMIT=100000 grove # one-shot, this invocation only
export GROVE_GITEA_BASE_URL=https://gitea.example.com # tickets.gitea.base_url
Precedence for one field, lowest to highest: the config file's value, the field's declared variable, then GROVE_<SECTION>__<FIELD>.
Worked example
The team's committed file, a teammate's gitignored theme, and another's personal agent compose with no coordination and no list wiped.
{
"worktree": { "root_template": "${repo}/.worktrees" },
"init_script": {
"enabled": true,
"inline": "uv sync",
"timeout_seconds": 180
}
}
See also
- Project setup: where each file goes.
- Agents: the merge-by-name rule in action.
- Configuration reference: every field, auto-generated from the model.