Skip to content

Proposal: declare team secrets in the repo, keep their values on each machine, for MCP servers and CLIs #875

Closed
1 of 1 issue completed

Description

@SaulMoro

Today. A team that shares a credential shares its value too. env/env.yaml holds each variable's value in plaintext, and teamai delivers it two ways: teamai pull resolves ${VAR} in mcp/mcp.yaml from it, and env.sh exports it to the member's login shell.

env/env.yaml     GITHUB_TOKEN: ghp_...          committed, in every clone and in git history
mcp/mcp.yaml     Bearer ${GITHUB_TOKEN}
env.sh           export GITHUB_TOKEN=...        what gh, glab or an internal CLI sees

every member     the same token, whoever it belongs to

A member can use their own token today only by exporting it in their shell. That breaks in two places:

  • MCP. The session-start pull runs in the agent's environment, which often lacks that export (a GUI-launched tool, or a zsh export under bash -lc), so it removes the MCP entry the interactive pull wrote.
  • CLIs. gh, glab or a CLI the company ships as a package read their token from the environment of the process the agent starts. env.sh is sourced once, when the shell starts, and doesn't follow the directory, so a member in two teams can't have gh use a different account in each.

Proposal. The team declares which secrets it needs. Each member supplies the value on their own machine, or already has it in their environment.

env/secrets.yaml    GITHUB_TOKEN, GITLAB_TOKEN    declared, no value

Alice               teamai env set GITHUB_TOKEN     her token, stored on her machine
Bob                 exports GITHUB_TOKEN already    used as is
Carol               nothing set                     server skipped, and she is told what to run
  • No secret value is written to the team repo.
  • A member in two teams, such as a personal one and their company's, keeps a different value for each.
  • A value the member already has in their environment is used without setting anything, and they can still override it for one team.
  • MCP servers and CLIs the agent runs get the same value.
  • A missing secret says which command fixes it, instead of a server that silently isn't there.
  • Plain env.yaml variables keep working. A member can override one for their team with teamai env set, and every key resolves in one order, for MCP servers and CLIs alike.

Goal. A team can share the credentials its members need, for MCP servers and for CLIs such as gh, glab or an internal tool, without putting a credential in git, and each member uses them with their own account.

Terms. A secret is an env key the team declares without a value. A scope is one teamai config on a machine: the user scope in ~/.teamai, or a project with its own .teamai/config.yaml. A team is one team repo; several scopes can use the same team.

flowchart LR
  D["env/secrets.yaml<br/>GITHUB_TOKEN declared"] --> R{{resolve per team}}
  S["team value<br/>teamai env set"] --> R
  G["machine value<br/>teamai env set --global"] --> R
  E["system environment<br/>export GITHUB_TOKEN=..."] --> R
  R --> M["mcp/mcp.yaml<br/>Bearer ${GITHUB_TOKEN}"]
  M --> C["Claude Code, Codex, OpenCode, ...<br/>MCP config"]
  R --> X["teamai env exec -- gh ...<br/>gh, glab, internal CLIs"]
  R --> N["not set anywhere<br/>server skipped, pull and doctor say what to run"]
Loading

The team declares its secrets next to its variables, in a file of their own.

# env/secrets.yaml
secrets:
  - key: GITHUB_TOKEN
    description: GitHub token with repo scope, for the github MCP server and gh
    url: https://github.com/settings/tokens     # where a member gets one
  - key: GITLAB_TOKEN
    description: GitLab token with api scope, for glab
  - key: ACME_TOKEN
    description: Token for the acme CLI

mcp/mcp.yaml doesn't change. A ${VAR} that names a declared secret resolves from the member's value.

# mcp/mcp.yaml, unchanged
servers:
  - name: github
    transport: http
    url: https://api.githubcopilot.com/mcp/
    headers:
      Authorization: Bearer ${GITHUB_TOKEN}

A CLI gets the value when it runs under teamai env exec, which resolves the scope from the current directory:

teamai env exec -- gh pr create
teamai env exec -- glab mr list      GITLAB_HOST from env.yaml and GITLAB_TOKEN from the member, for this directory's team

A namespace declares its own secrets in env/<ns>/secrets.yaml, activated by resources.env like env/<ns>/env.yaml. A namespace entry replaces the root entry with the same key.

A member sets a value once per team, or once for the whole machine. The value is read from a hidden prompt, from stdin or from another variable, never from an argument, so it doesn't land in shell history.

teamai env add GITHUB_TOKEN --secret -d "..." --url <url>    declare it in the team repo, no value (admin)
teamai env set GITHUB_TOKEN                                  value for this directory's team
teamai env set GITHUB_TOKEN --from-env WORK_GITHUB_TOKEN     read it from another variable, no copy stored
teamai env set GITHUB_TOKEN --global                         value for every scope on this machine
teamai env set GITHUB_TOKEN --stdin                          for a member's own scripts
teamai env unset GITHUB_TOKEN [--global]
teamai env list                                              each secret: team, global, environment, or missing
teamai env exec -- <command>                                 run a command with this scope's env and secrets

Resolution, per key and team, the same for ${VAR} in MCP servers and for env exec:

value set for this team             teamai env set KEY [--from-env VAR]      secrets and variables
> value set for the machine         teamai env set KEY --global              secrets only
> the value in env.yaml             the team's value                         variables only
> the member's own environment      not a value a teamai `env.sh` exported   secrets only
> missing                           the server is skipped, or keeps the entry an earlier pull wrote

A team value wins over the environment because it is an explicit choice for that team. Otherwise a personal GITHUB_TOKEN exported in .zshrc would override the company token a member set for their work project. For the same reason, the environment no longer overrides a plain env.yaml variable in MCP servers, as it does today: a GITLAB_HOST exported for one team would reach another team's server. A member who overrides a variable with export today moves to teamai env set KEY, and pull says so when it ignores an exported value that differs from the team's. env.sh then exports the member's value, so the shell follows the same order.

When a secret is missing, interactive pull, doctor, env list and mcp list name the secret, the server that needs it if there is one, and the fix:

github: GITHUB_TOKEN is not set. Run `teamai env set GITHUB_TOKEN` (https://github.com/settings/tokens).
GITLAB_TOKEN is not set. Run `teamai env set GITLAB_TOKEN`.

The silent session-start pull doesn't print it, since it runs on every session. doctor reports it as a warning, not a failure. env exec prints the same line to stderr and runs the command anyway, since gh and glab can still use their own login (gh auth login).

What I'd put in v1.

  • env/secrets.yaml and env/<ns>/secrets.yaml, with key, description and an optional url.
  • env add --secret, env set [--global] [--stdin] [--from-env VAR], env unset [--global], and the secret state in env list. Without --global, env set also overrides a plain variable for this team.
  • env exec -- <command>, with this scope's variables and secrets.
  • Resolution in the order above, for ${VAR} in MCP servers and for env exec, replacing today's "the environment wins" for plain variables in MCP servers, with the pull notice for an ignored export.
  • A declared secret that one pull can't find keeps the MCP entry an earlier pull wrote.
  • The actionable message in interactive pull, doctor, env list, mcp list and env exec.
  • The session-start hook tells the agent which secrets the scope declares and to run the CLIs that need them through env exec.
  • docs/designs/team-secrets.md and team-secrets.zh-CN.md, shipped with the code the way model-profiles.md was: storage, resolution order, delivery to MCP and env exec, and what stays reachable.
  • The existing docs and agent skills updated (see Docs and skills in the design notes).

What could wait.

  • The OS credential store (Keychain, Credential Manager, Secret Service) instead of a 0600 file.
  • Exporting secrets to the shell through env.sh.
  • Writing third-party config files, such as ~/.npmrc for a private registry. A project .npmrc that reads ${NODE_AUTH_TOKEN} already works under env exec.
  • A hook that wraps the agent's commands in env exec automatically.
  • Team-shared secrets encrypted in the repo (sops/age).
  • HTTP mode, which has no env channel today.
  • Secrets for remote executors and cloud sandboxes (Discussion: Workflow Runtime Integration #818), where there is no ~/.teamai and no member environment.

Acceptance check. Alice belongs to a personal team in her user scope and to her company's team in ~/work/api. Both declare GITHUB_TOKEN. She runs teamai env set GITHUB_TOKEN --global with her personal token, then teamai env set GITHUB_TOKEN --from-env WORK_GITHUB_TOKEN in ~/work/api. The github server in her personal team uses the personal token and the one in ~/work/api uses the work token, and teamai env exec -- gh api user returns her personal account in her home directory and her work account in ~/work/api. Bob already exports SENTRY_AUTH_TOKEN and GITLAB_TOKEN in .zshrc and sets nothing. His interactive pull adds the sentry server, a session-start pull that doesn't see the export leaves it in place, and teamai env exec -- glab mr list uses his exported token. Carol sets nothing. Her pull skips github and tells her the command to run, and when she asks her agent about it, the agent asks her to run that command in her own terminal. Dave's team sets GITLAB_HOST in env.yaml, and he exports a different one. His pull keeps the team's value and tells him to run teamai env set GITLAB_HOST; after he does, the MCP server, teamai env exec -- glab and a new shell all use his. In a directory with no scope, teamai env exec -- env shows only the inherited environment and a notice on stderr. No token appears in the team repo or in git log -p.

Design notes: proposed details and open questions

Proposed:

  • Separate file. teamai env add and env remove rewrite env.yaml with only its variables: list, so a member on an older CLI editing env would drop a secrets: key in the same file. Older CLIs read only env/env.yaml and env/<ns>/env.yaml, so they ignore env/secrets.yaml entirely. A variable without a value isn't an option either: it fails the schema, and with it the whole env resolution for every member on an older CLI.
  • Storage. Values live in files of their own under ~/.teamai/secrets/ (~/.teamai/env is already the user scope's env backup), apart from model profile keys: machine.json for the machine, and teams/<hash>.json per team repo, keyed by the same repository-identity hash teamai models configure uses, but without the team's display name, so renaming team: in teamai.yaml doesn't orphan anyone's values. A team used from several projects or worktrees then needs one env set. Each entry is either a literal value or the name of a variable to read (--from-env). They always live under ~/.teamai, never in the scope's data directory, since in single-repo mode that sits inside the business repo. Files are written atomically with mode 0600, like the model key files.
  • Model profile keys. They stay where they are. api_key: ${API_KEY} in models/models.yaml is a slot per profile, not a variable: every profile uses the same placeholder, and its key is bound to the profile and its gateway's origin (feat(projects): extend project isolation to knowledge/docs/agents/env/hooks/mcp #707). It is written into each agent's machine-wide config, so it can't differ per directory. teamai models configure keeps storing it under ~/.teamai/models/. env set doesn't configure it, and env/secrets.yaml can't declare it. models configure --from-env VAR still reads only the process environment, not a value set with env set. The two share code, not files: the model key files are parsed strictly, so a secret entry there would break every models command on an older CLI.
  • env exec. It resolves the scope from the current directory with detectProjectConfig, by git anchors, so every worktree of a project resolves to that project, and falls back to the user scope. Then it runs the command with the inherited environment, overlaid with this scope's variables and secrets resolved in the order above, so a GITLAB_HOST exported for another team doesn't leak in. It writes no value to disk. A variable or secret named like one a model profile writes (ANTHROPIC_*) overrides that profile for the command, so env exec is for CLIs, not for starting an agent.
  • Outside a scope. env exec in a directory with no teamai scope runs the command with the inherited environment and says on stderr that no scope was found. Machine values aren't applied there, since no team declares which keys the command needs.
  • Workflows (Discussion: Workflow Runtime Integration #818). A workflow step that lists requires: { env: [GITHUB_TOKEN] } refers to the key declared here and resolves it in the same order. The step declared that it needs the key, so a missing one fails the run before it starts, instead of warning as env exec does. In CI there is nothing under ~/.teamai, so the value comes from the job's environment, the last step of the order. No value is written to run history or sent to an executor.
  • Telling the agent. An agent that runs gh without env exec silently uses whatever account its environment has. When the scope declares secrets, the session-start hook adds one line to the agent's context through additionalContext, the way the MR hint does, listing each key with its description and saying to run those CLIs through teamai env exec --. Agents without a SessionStart hook get the same rule from the core skill.
  • Keeping the entry. The session-start pull inherits the agent's environment, so a secret that only lives in the member's shell is there for one pull and gone for the next. Today a missing ${VAR} removes the managed entry. For a declared secret, a pull that can't find it keeps the entry as it is, the way a file that doesn't parse changes nothing today, and the way a model profile keeps an agent's settings when a pull can't see its --from-env variable. The entry is removed only when its server leaves mcp.yaml. A rotated or removed secret then keeps the old value until a pull finds the new one, and doctor says so. Variables that aren't declared as secrets keep today's behaviour.
  • Not in env.sh. Secrets are never written to env.sh. It is sourced by every shell, is written with the default umask, and the profile loads one scope at a time (env: a project pull drops the user scope's env block from the shell profile #876). env exec covers the per-scope case, and a member who wants the token in every shell already has it in their environment.
  • Same key twice. A key declared as a secret and also set as a variable in env.yaml resolves as a secret, and the repo value is ignored. pull and doctor warn about it. An older CLI keeps using the variable, so nobody loses the server while the team removes the value.
  • env set checks. In a scope, env set accepts only keys that scope declares as secrets or receives as variables, and --global only secrets. env set --global outside any scope accepts any valid key name and notes that no team declares it yet, so a member can set a token they reuse across teams ahead of time.
  • No migration. A team that committed a token has to rotate it, since git history keeps it. The docs say so, and that's all v1 does about it.
  • Agents. The agent skills say that an agent never asks for a secret value in chat, never passes one through --stdin, never reads the values file, and never prints or quotes a secret. When doctor, pull, mcp list or env exec report a missing secret, the agent asks the member to run teamai env set KEY in their own terminal. Declaring a secret with env add --secret is safe for an agent, since it has no value.
  • Docs and skills. docs/usage-guide.* (including its MCP Secrets paragraph, which says the environment wins), docs/product-overview.*, a link from docs/designs/model-profiles.* to the new design doc for Model profile keys, docs/designs/multi-project-management.md and docs/designs/management-backend.* in both languages; skill-data/core for members, including when to use env exec; skill-data/setup/references/manage-admin.md for admins; skill-data/core/references/commands.md regenerated. No README change.
  • Still reachable. The resolved value is still written in plaintext to each tool's MCP config, as today (new files are created 0600). A command under env exec gets it in its environment, so an agent that runs teamai env exec -- env can read it; the skills forbid it, but nothing enforces it. An agent that can run gh with a token can already do whatever the token allows. This keeps secrets out of git, not away from the member's machine or the agent it runs.
  • Not bound to a server. A secret reaches whatever server mcp.yaml names, as ${VAR} does today. Unlike model profile keys (feat(projects): extend project isolation to knowledge/docs/agents/env/hooks/mcp #707), it isn't tied to a host, so whoever can change mcp.yaml or add a namespace decides where members' tokens go. Whoever can push to the team repo already ships hooks that run on every member's machine, so v1 accepts this and the docs say so.
  • Rejected. Writing secrets into each agent's config (Claude settings.json env, .claude/settings.local.json): only Claude injects them, and the value would sit in plaintext inside the business repo's directory. A used_by field: description already says which tool uses the key, and the field would drive no behaviour. A hook that rewrites the agent's commands to add env exec: it depends on each agent and means parsing shell commands.
  • Code seams. Team key files and the { value } | { env } shape: getTeamValuesPath and resolveProfile in src/models/profile.ts. MCP variable table: buildVarTable in src/mcp-reconcile.ts. Session context: the additionalContext merge in src/hook-dispatch.ts, as src/mr-hint.ts uses it. Scope for a directory: detectProjectConfig in src/config.ts.
  • Prior art. Executor keeps credentials apart from app source and lets a member keep a work and a personal account for the same service. This takes the part that fits a git-backed tool. Inside teamai, the models channel already declares api_key: ${API_KEY} in the repo and keeps the value on each machine, per team. Secrets reuse that shape, not its files; see Model profile keys.

Decided for v1, open to feedback until the spec is merged:

  • doctor warns, it doesn't fail, when a delivered MCP server needs a missing secret.
  • The link field is url.
  • The command is env exec.

Feedback from anyone whose team shares credentials for MCP servers or CLIs would help:

  • Does a value per team plus one per machine cover how you split personal and work accounts?
  • Do your members already keep these tokens in their shell environment, or would they rather set them with teamai?
  • Which CLIs does your team need a per-team token for?
  • Is anything here more than a first version needs?

Activity

  1. changed the title [-]Proposal: declare team secrets in the repo, keep their values on each machine[/-] [+]Proposal: declare team secrets in the repo, keep their values on each machine, for MCP servers and CLIs[/+] on Sep 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions