Repository navigation
[feat] Workspace-scoped env overrides, so one machine can point each repo at its own database #1429
Description
Problem
A team can declare environment variables and secrets in the team repo (env/env.yaml, env/<ns>/env.yaml), and each member resolves their own value into one flat store. But that store is keyed by team + machine only:
// src/env.ts
function getTeamSecretsPath(localConfig) {
const identity = configured ? repoIdentity(configured) : localPath;
const hash = sha256(identity);
return path.join(getTeamaiHomeDir(), "secrets", "teams", `${hash}.json`);
}There is no workspace/project component in the identity, and teamai env set has no --project or --role flag. So a given key can hold exactly one value on a machine.
That breaks the most common multi-repo setup: one machine, several repos, each pointing at its own database instance. Concretely, a member with
~/work/service-a -> postgresql://...@127.0.0.1:5432/a
~/work/service-b -> postgresql://...@127.0.0.1:7432/b
cannot express this. PG_DATABASE_URI resolves to whichever value they set last, in both directories. The rendered MCP server inherits that single URI, so one of the two repos silently gets the wrong database — and because the schema still looks plausible, the failure is a wrong answer rather than an error.
The current workarounds are all unsatisfying:
- Per-instance namespaces (
mcp/postgresql-a,mcp/postgresql-b, each readingPG_URI_A/PG_URI_B). This works, but it grows one namespace and one key per instance. A team with 10+ database instances ends up with 10+ near-identicalmcp/namespaces differing only in an env key name, purely to work around the flat store. - Injecting values from the shell.
teamai env set KEY --from-env VARreads the live environment each time, so adirenv/dotenvsetup can give each directory its own value. This is the closest thing to a solution today, but it is undocumented as a multi-directory pattern, it is invisible toteamai env list, andpullreports the variable as set without any hint about which directory produced the value. - Rewriting the partition
env.shfrom apostPullscript. Works, but bypasses the delivery record, sopullhas no idea what it just wrote.
Proposed Solution
Make env values layered, with a workspace-scoped override layer between the team declaration and the resolved value — the same model as application-local.yml or .env.local:
1. team declaration env/env.yaml, env/<ns>/env.yaml (in repo, shared)
2. workspace override <partition>/env.local.yaml (this machine, this directory)
3. member value ~/.teamai/secrets/teams/<hash>.json (this machine, this team)
4. resolved -> rendered into MCP / env.sh
Three design points I think matter more than the mechanism itself:
a. Overrides must be set, not declare.
A workspace override should only be allowed to supply a value for a key the team already declares. Overriding an undeclared key would silently add environment surface the team cannot see, and hand any repo a way to inject arbitrary variables. Reject unknown keys the way keepScopedEntry already rejects unknown YAML keys today.
b. An override is not a second declaration.
The existing rule — the same key in two simultaneously-active env namespaces rejects the whole env set (env was not applied this run) — must not fire for an override. An override is a precedence fact about one key, not a competing declaration. Otherwise the feature contradicts itself the first time it is used.
c. Overrides live outside the repo, keyed the same way the partition is.
~/.teamai/projects/<slug>-<hash>/env.local.yaml, alongside the existing config.yaml and env.sh in that directory. A file inside the workspace would eventually get committed — every repo would need its own correct .gitignore entry, and one missing entry is a leaked password.
Sketch of the CLI surface:
# in a workspace, override the value for this directory only
teamai env set PG_DATABASE_URI --workspace
# inspect the layering, showing which layer won
teamai env list --origin
# PG_DATABASE_URI ... (workspace) ~/.teamai/projects/<slug>-<hash>/env.local.yaml
# DB_HOST ... (env.yaml) env/env.yaml
# STITCH_API_KEY ... (secret) ~/.teamai/secrets/teams/<hash>.json
# drop the override and fall back to the member value
teamai env unset PG_DATABASE_URI --workspaceteamai env list --origin is the part I would care about most as an operator: right now, when an MCP server connects to the wrong database, there is no single command that tells you which of the four layers produced the value.
Alternatives Considered
- Do nothing; document per-instance namespaces as the answer. This is what a downstream team is doing today. It works for 2–3 databases and becomes unmaintainable past that; it also conflates "which server to run" with "which value to inject", which are independent concerns.
--projectonteamai env set. This would key the store by project id instead of workspace path. It is closer to the existing model, but a project id is a team-repo concept — the same project id can be checked out at several paths, and a member may want two checkouts of the same project pointed at different databases. Directory-scoped is the honest granularity.- Resolve values from the shell only (
--from-enveverywhere). Zero new surface in teamai, but it moves the configuration out of teamai entirely;env listcan no longer explain a value, and onboarding now requires the member to install and configuredirenv.
Additional Context
- The README of our team repo already calls out the constraint explicitly ("
teamai env set的值按「团队 + 本机」唯一存储…同一键名在整台机器上只能有一个值"), and routes people to per-instance namespaces. We would rather not ship that guidance as the long-term answer. - Related: [feat] Support Antigravity CLI (agy) as a delivery target #1426 (Antigravity CLI support) is from the same deployment; the multi-repo, multi-stack directory layout there is exactly the situation this issue describes.
- Happy to prototype the
env.local.yamlread path and the--originlisting if the layering approach is acceptable.
Published from a team-level teamai deployment (user scope, several repos on one machine, each with its own database instance).
Activity
SaulMoro commentedon Oct 26, 2026
Thanks for the detailed write-up, and for offering to prototype.
Triage outcome: new enhancement. No existing issue or open PR covers it. The core behavior reproduces on 0.28.0 and on
main: two repos with their own project scope share one member store, so both resolvePG_DATABASE_URIto the value set last, inteamai env --reveal, in each.mcp.jsonand inenv exec. Labels:enhancement,question.Corrections to the premises
getTeamSecretsPathinsrc/secret-store.ts:60-68. There is nosrc/env.ts. Your description of the key (repo identity, no scope or directory) is right.src/utils/partition.ts:164-170). Two separate clones get two partitions; two linked worktrees of one repo get one.pullwrote no.mcp.jsonin either repo, and thepgserver in~/.claude.jsonheld whichever value was pulled last. A per-repo value there needs per-repo scopes, or per-project entries in~/.claude.json(which teamai already uses for local scopes, [feat] keep the resources pull delivers in project scope out of git #915).env listalready shows where a value comes from. Each variable showsteamorenv.yaml, each secret showsteam,global,environment,missingorunreadable, and both show their namespace (rootor<ns>) (src/env-listing.ts:86-104). What it does not show is the--from-envvariable name or the directory.--from-env) already works per repo for MCP in project scope. Each repo's.mcp.jsongot the value exported at that repo'spull. Noenv.shline is written for a--from-envkey, by design (src/env-resolution.ts:38-45), so a shell gets the value only from its own environment.Approach, at a high level
resolveTeamEnv), soenv list,env.sh, the MCP variables andenv execall agree on the value.env setalready refuses undeclared keys, which is the precedent to follow.config.yamlandenv.sh, as you propose.env listalready prints with the new layer, before adding a flag.Questions to settle before a PR
--from-enventry, and for secrets--globaland the environment? Keys a project inherits from user scope are resolved separately, so the order has to cover them too.Once these are answered, this can be opened for a contributor.
Testing
This changes runtime behavior, so AGENTS.md ("Testing before a PR") asks for one end-to-end record from the real CLI after
npm run build: two repos with their own scope, a differentPG_DATABASE_URIoverride in each, thenpull. Each repo should then show its own value inteamai env --reveal,.mcp.json,env execand its partitionenv.sh. Useenv --revealrather thanenv list --reveal, which currently keeps values masked (#1432).Related: #875 and #879 (team secrets design), #1018 / #1023 (team env by directory, per scope), #913 (workspace children).