Files
Nicholas TindleandClaude Opus 5.5 09a24074ec dx(backend): add a helper that syncs selected env keys between machines (#16266)
### Why / What / How

**Why:** the Microsoft app's three settings (`MICROSOFT_CLIENT_ID`,
`MICROSOFT_CLIENT_SECRET`, `MICROSOFT_TENANT_ID`) are shared by every
developer machine that runs the Teams bot or the Microsoft connection.
Copying them between machines by hand means pasting a secret through a
terminal or chat.

**What:** a small developer script, `python -m scripts.env_sync`, that
compares chosen keys in env files on other machines (over SSH) with a
local env file, and copies missing values only when asked. It never
prints a value.

**How:**

- Reads each `--source` (`host:/path/.env`, or `local:/path`) as UTF-8,
keeping only the allowlisted keys. Without `--key`, it uses the three
Microsoft settings. Hosts named with `--windows-host` (default:
`desktop`) are read through PowerShell, with the path quoted for its
tokenizer, curly quotes included.
- A dry run by default, reporting each key as `matches`, `different`,
`missing at target` or `missing at source`. `--apply` writes missing
values. Replacing a different non-empty value also needs `--replace`.
Sources that disagree are rejected. An empty or missing source value
never clears the target.
- Which keys change, and whether that replaces a value, is decided from
the target as read under its `.sync-lock`. A value another run wrote
after the report therefore still needs `--replace`.
- Writes go to a local file only, through an atomic replace. The
previous file is first copied to `~/.local/state/autogpt-env-sync/` (or
`$XDG_STATE_HOME`), a 0700 directory outside any repository, so `git
add` cannot stage the copy. The edit is drafted there too.
- A sync killed mid-write can leave only the lock and the `.env-sync-*`
replacement beside the target, and `.gitignore` covers both. A leftover
lock stops later runs with a message naming it.
- Tracked files, symlinks and template env files (`.env.default`,
`.example`, `.sample` and the like) are refused, by the dry run too.
Suffixes match in any case. The tracked check ignores inherited
`GIT_DIR`-style and pathspec variables, matches the name literally, and
stops on any git answer it doesn't recognise.
- `docs/engineering/env-sync.md` explains it.

### Changes 🏗️

- `backend/scripts/env_sync.py`, with tests in `env_sync_test.py` (CLI
and reading) and `env_sync_apply_test.py` (writing a target).
- `docs/engineering/env-sync.md`.
- `.gitignore`: `.env-sync-*` and `*.sync-lock`.

### Agents and large language models used

- Claude Code with Claude Opus 5.5: implementation, review fixes and
tests.

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
- [x] `pytest scripts/env_sync_test.py scripts/env_sync_apply_test.py`:
39 passed. They cover value selection and conflicting sources, the
dry-run statuses and refusals, and Windows hosts with PowerShell
quoting. They also cover non-ASCII values under an ASCII locale,
`--replace`, and backups kept out of the repository and private. The
race with another run, a sync killed at either file replacement leaving
nothing `git status` lists, the stale-lock message, a failing read
printing nothing of its output, and the refusal of symlinked, template
and tracked targets (also in any case, under an inherited `GIT_DIR` or
pathspec setting, with an option-like or pathspec-like name, or when git
fails) are covered too.
- [x] Ran the generated PowerShell read command against a Windows host
with a path containing all five quote characters PowerShell ends a
string at.
- [x] Used it to compare the Microsoft settings across three machines
without printing them.
  - [x] Black, isort, pyright and the secret scanners pass.

#### For configuration changes:

- [x] `.env.default` is updated or already compatible with my changes
- [x] `docker-compose.yml` is updated or already compatible with my
changes
- [x] I have included a list of my configuration changes in the PR
description (under **Changes**): `.gitignore` gains two patterns for
files a killed sync can leave.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (Claude Code) <[email protected]>
2026-10-27 23:13:28 +00:00

2.1 KiB

Syncing selected development settings

scripts/env_sync.py copies chosen keys from env files on your other machines into a local one, over SSH. It never prints a value. Without --key, it syncs the three settings the Microsoft app shares between the backend and the Teams bot: MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET and MICROSOFT_TENANT_ID.

From autogpt_platform/backend:

poetry run python -m scripts.env_sync \
  --source 'mac:/path/to/backend/.env' \
  --source 'desktop:C:\path\to\backend\.env' \
  --target .env

This is a dry run: it reports each key as matches, different, missing at target or missing at source. Add --apply to write the keys missing at the target. Replacing a different value also requires --replace. Sources with conflicting values are rejected: choose one explicitly. Empty or missing source keys never clear the target. Use repeated --key KEY_NAME to select another explicit allowlist. Name the SSH hosts that run Windows with repeated --windows-host HOST; without it, desktop is the only one.

Applying checks the target again under a lock just before writing, so a value another run added since the report still needs --replace. It then copies the existing target to ~/.local/state/autogpt-env-sync/ ($XDG_STATE_HOME/autogpt-env-sync/ when that is set), a directory only you can read and outside any repository, so git add cannot stage the copy. It prints the copy's path. The script never deletes these copies, and each holds every setting the target had, so remove old ones you no longer need. Finally it replaces the target atomically, keeping unrelated settings.

Tracked files, symlinks and template env files are rejected, by the dry run too. Only local targets are writable; remote environments stay unchanged. Choose a canonical source before enabling any scheduled sync.

A sync that is killed mid-write can leave <target>.sync-lock, and possibly an .env-sync-* file, beside the target. This repository's .gitignore covers both; for a target in another repository, add the two patterns to its .gitignore. Later runs stop and name the lock; delete it once no sync is running.