The registry is the only source of environment facts. An agent that takes a host, branch, command, or password from memory or chat history will one day run a staging command on prod.
1. Load and maintain the registry
- Read
.agents/environments.jsonat the repo root, in full, before the first environment action of the session. - Missing, and the task needs a non-local environment: run section 10 first. Do not improvise hosts or commands in the meantime.
- When the user states a new fact (server, account, URL, rule), put it in the registry instead of keeping it in chat.
- Every registry change is shown as a diff and written only after a yes; it applies from the next action. Changes that weaken safety (
policy,allowedWithoutAsking,data.testDataAllowed,rules,defaultEnvironment,integrationBranch, or any command of aconfirm-each-stepenvironment) need the user to ask for that change explicitly. - A string
logs(andlogsinallowedWithoutAsking) is the old name oflogsCommand: propose the rename as a registry change; it is a rename, not a new command or a weakened safeguard. version,verify, andlogsCommandcommands must be read-only; refuse to register one that writes. A request to skip a safeguard for one task ("just run it all", "create a test user on prod") is not a request to change the registry: keep the safeguard, and offer the registry change.
2. Name the target environment
- Use the environment the user named, otherwise
defaultEnvironment. - Never reach a
confirm-each-stepenvironment by default or inference. If a request could mean prod ("live", "the real site", "customers see this"), ask which environment. - Prefix every announced command and every result with the environment:
[staging] supabase db push .... - Right before any write, check that the target belongs to that environment in the registry, without printing secrets:
- SSH: the alias in the command equals
access.ssh. - Vercel:
.vercel/project.jsonprojectIdandorgIdequalaccess.vercelProjectIdandaccess.vercelOrgId. - Supabase:
[[ "$PROD_DB_URL" == *<supabaseProjectRef>* ]] && echo match || echo MISMATCH, and the--project-refin the command equals the registry. - Any mismatch: stop and report.
- SSH: the alias in the command equals
3. Policies
policy | Reads | Writes (deploy, migration, restart, env vars, data) |
|---|---|---|
autonomous | Free | Free; report afterwards |
confirm-writes | Free | Show the whole plan, get one confirmation, run it; stop and ask on any unexpected result |
confirm-each-step | Ask first, except registry commands listed in allowedWithoutAsking (empty by default) | Section 4, every step |
allowedWithoutAskinglists registry commands by name (version,logsCommand,verify), pluslogsQueryfor bounded Better Stack reads perbetterstack-logssection 4; any other remote command, including ad-hoc SQL, asks first. Local reads (git, files in the repo,op whoami) are always free.rulesadd to the policy. When a rule is stricter, follow it; when it is looser than the policy, follow the policy and point out the conflict. When a rule depends on something you cannot observe (a customer event, telling the team), ask the user before acting.data.testDataAllowed: falseis a kill switch: never create, modify, or delete records in that environment, except through reviewed migrations shipped by the deploy protocol. Where test data is allowed, puttestDataMarkeron every record you create and delete those records when done.- Approval never carries over: not from an earlier session, an earlier deploy, or another environment.
- Before showing remote logs or query results in chat, mask secrets, tokens, and personal data such as emails and phone numbers.
- A service's
logsobject names its Better Stack source; reading an environment's logs followsbetterstack-logssection 4, under this environment's policy.
4. confirm-each-step protocol (prod)
Placeholders in registry commands:
<sha>: the full 40-character SHA being deployed.<previous-sha>: the code version captured in pre-flight fromversion; rollback depends on it. For a database,versionreturns the latest applied migration instead; capture it too.<timestamp>: UTCYYYYMMDD-HHMM.- Anything else (
<migration-name>, ...): filled in the plan where the user can see it.
Before the first step:
- Pre-flight, read-only: capture every service's
version, resolve<sha>, list pending migrations, check every rule (deploy windows, events), and create the clean deploy checkout (git worktree add .agents/deploy/<sha> <sha>); commands that read the local tree run from there. - Database or data changes in the deploy: complete the section 8 checklist first.
- Prove what was tested is what ships: the tree at
<sha>must equal the tree that passed on staging (git diff --stat <staging-sha> <sha>is empty). Proof that staging passed is an E2E evidence file or the user's explicit word. - Present a numbered plan: services in
deployOrder(default db before web), then each service'sdeployitems in order, one item per step. For every step, show the exact command (secrets masked), what it changes, its check, and its undo. Where the registry has no per-step check or undo, propose one and mark it "proposed"; the plan approval covers it. A step may be skipped only when the plan says why (e.g. no function changed). - Get approval of the plan. Approving the plan does not approve any step.
For each step:
- Show the exact command and ask "Run step N?"; run nothing until the answer is yes.
- Run only that step, show the relevant output, then run its check.
- Non-zero exit, unexpected output, or a failed check: stop. Do not run the next step and do not invent a fix. Report what state each service is in now (e.g. migrations applied but web still on
<previous-sha>), present the options (retry, undo, roll back, investigate), and wait. - Never merge steps to save confirmations: no
&&across steps, no ad-hoc scripts, no interactive SSH session.
After the last step: run every verify; code services' version must print <sha>, and the database's must equal the newest migration at <sha>. Report what ran, the running versions, the check results, and the rollback commands with placeholders filled in.
5. Deploy and access servers
- Use the registry's commands exactly as written, substituting only placeholders.
- A remote service needs
version,deploy,verify, androllback;logsCommandwhere the platform has a CLI for it. One is missing: propose it fromreferences/deploy-targets.md, get it confirmed, write it into the registry, then use it. - SSH only through the alias in
access.ssh(defined in~/.ssh/config), one remote command per call:ssh <alias> '<cmd>'. - Docker Compose on a server (shared hosts above all): follow the
server-touchskill. - "Pushed" is not "deployed": a deploy is done when
versionmatches the target andverifypasses. - Read the section of
references/deploy-targets.mdfor each service type you touch; it holds the platform traps (Supabase link state, Vercel git vs. CLI deploys, VPS release folders).
6. Version status across environments
When asked what runs where ("môi trường nào đang chạy bản nào", "prod đang ở commit nào"), or before planning a promotion:
git fetch origin --prune, then run every service'sversion, environment by environment. On aconfirm-each-stepenvironment this asks first unlessversionis inallowedWithoutAsking.- Report one table: environment, service, short SHA, commit subject and date (
git log -1 --format='%h %s (%cr)' <sha>), and how far behind the integration branch it is (git rev-list --count <sha>..origin/<int>). - For each promotion path, list what would ship:
git log --oneline <prod-sha>..<staging-sha>. - Flag problems instead of smoothing them over:
unknownor an error: the service has no working version endpoint (references/version-endpoint.md).- A SHA that no remote branch contains (
git branch -r --contains <sha>is empty): someone deployed unpushed code. - Services of one environment on different SHAs, when they deploy together.
7. Testing per environment
testing.scope says what tests may run there:
scope | Allowed |
|---|---|
full | Any test, including destructive ones, data resets, and load tests |
non-destructive | Functional and E2E tests on records you create (with testDataMarker) and delete afterwards; no data resets, load or stress tests, bulk imports, or changes to other people's records |
smoke-only | Read-only flows with the listed accounts: open pages, search, view; no submit that creates or changes data, no payment, no message to a real person |
none | No tests |
- Missing
testing:smoke-onlyon aconfirm-each-stepenvironment,non-destructiveelsewhere. Whentesting.scopeanddata.testDataAlloweddisagree, follow the stricter one. testing.rulesadd project specifics (sandbox keys, QA hours, which flows make up the smoke test).- On
smoke-only, list the flows you will run and get a yes before opening the browser; a failure is reported, never fixed or retried by changing data on that environment. - External side effects cause most testing harm: trigger payments only with test-mode keys, emails or SMS only to addresses the team owns, webhooks only to sandboxes. If you cannot confirm that, do not trigger the flow; ask.
- Changing shared settings (feature flags, admin config) counts as a data change: record the old value first and restore it when done.
- Evidence follows the
e2e-verifyskill.
8. Database and data changes
Applies to every migration, backfill, data fix, import, bulk admin action, or manual SQL against a confirm-writes or confirm-each-step environment.
Classify every statement first:
| Class | Examples | Extra gate |
|---|---|---|
| Additive | CREATE TABLE, nullable ADD COLUMN, CREATE INDEX CONCURRENTLY | The checklist |
| Data-changing | UPDATE, DELETE, backfill INSERT, data fix scripts | Row counts and a copy of the affected rows |
| Destructive or locking | DROP, RENAME, ALTER ... TYPE, SET NOT NULL, new FK or unique constraint on a large table, TRUNCATE | An expand/contract plan and a separate yes per statement |
Before the deploy plan, present this checklist filled in with evidence. On confirm-each-step, any line answered "no" or "unknown" stops the deploy until the user resolves it; on confirm-writes, it is raised in the plan.
- Backup: method (point-in-time recovery or schema + data dumps), when it was taken, and proof it is usable (PITR restore point after the last change, or both dump files present and non-empty). A fresh backup is the first step of the deploy, not a claim about last night.
- Migration approach: backward-compatible with the code running now and the code being deployed (expand now, contract in a later release), and the deploy order between database and code.
- Tested on staging: the same migration files ran and passed there (
git diff --stat <staging-sha> <sha> -- supabase/migrationsis empty), ideally on a realistic data volume. - Dry run: the list of migrations about to apply equals the expected list; no surprise migration.
- Data changes: per data-changing statement, its WHERE clause, the expected row count, and where the affected rows are copied first (backup table or export). Measure counts without changing anything:
BEGIN; <statement>; <count check>; ROLLBACK;orSELECT count(*)with the same WHERE, each asked first on prod. Never anUPDATEorDELETEwithout a WHERE. - Locks and duration: tables touched, their row counts, and which statements rewrite or lock them; large tables need batching,
CONCURRENTLY, or a maintenance window. - Rollback: the reverse migration is written and reviewed before deploying. If the change cannot be reversed and restoring the backup is the only way back, the user must accept that explicitly.
- Side effects: triggers, RLS policies, cron jobs, realtime subscriptions, and functions or webhooks that fire on changed rows (an
UPDATEthat sends customer emails). - Timing and people: inside the allowed window, and who has been told (ask the user).
Manual data fixes belong in a reviewed migration or script in the repo, not typed into a SQL console. Run a fix in two calls: first with ROLLBACK to show the affected count, then, after the user confirms the count, the same statements with COMMIT.
9. Secrets and test accounts
- The registry is committed and holds no secret values; the schema rejects literals in fields named like password, token, secret, key, DB URL, or connection string.
- A secret is a reference:
{"secret": "env:NAME"}: from the shell environment or the gitignored.agents/secrets.env; commands use it as"$NAME". Every shell call starts fresh, so load the file in the same call:set -a; . .agents/secrets.env; set +a; <cmd>. If the file holdsop://values, useop run --env-file .agents/secrets.env -- <cmd>instead. Loading secrets this way does not count as changing a registry command.{"secret": "op://vault/item/field"}: 1Password, read on demand without printing, assigned and exported in the same shell call:NAME="$(op read 'op://...')"; export NAME; <cmd>.
- Never let a value appear in output, files, commits, command lines as a literal, logs, tickets, or chat, unless the user asks to see it.
- Browser logins: prefer a saved session (Playwright storage state) per account; when a password must be typed, it goes only into the login field, never into a reply.
- A reference does not resolve: tell the user which reference and why (
op whoamifails = not signed in; item not found = missing entry; env var empty = missing line in.agents/secrets.env). Ask them to fix it there, not to paste the value into chat, and do not fall back to another account. - Pick accounts by
rolefrom the target environment only. Never log in as a real customer; on prod use only the accounts listed there.
10. Create the registry
- Draft from repo evidence first: existing environment docs (
.claude/environments*.md,docs/**/environments*.md, environment notes inAGENTS.md,CLAUDE.mdor README),.env*.example(variable names only),.vercel/project.json,vercel.json,supabase/config.toml,Dockerfile, compose files,ecosystem.config.*, CI workflows, deploy scripts. An existing environment doc is a source to import, not a question: take its environments, URLs, branches, SSH aliases, policies or rules, and account roles and logins into the draft. A secret value in it is never copied: it becomes anenv:reference, and moving and rotating it is waiting on the user. Leave the doc in place; the report lists what was imported from which file and proposes deleting it or replacing its body with a pointer to.agents/environments.json, so the two cannot drift. - Ask the user only what the repo cannot tell, in one batch, your drafts shown; it always starts with these, unless the registry already records them or a calling skill asked them in this run (
project-initsection 2, or theharness-mapsetup batch in its section 3, whose answers are taken as given):- Which branch PRs/MRs go into (
integrationBranch,worktree-flowsection 0), never assumed. - Which environments exist: local or dev, staging, prod, and any other, each with its URL and the branch it deploys from. Then SSH aliases, test account roles and logins, rules such as deploy windows.
- Which branch PRs/MRs go into (
- Write
.agents/environments.jsonbased ontemplates/environments.json, withintegrationBranchset, and copytemplates/environments.schema.jsonnext to it. When a calling skill's batch showed the drafted environments and the user answered it, that answer is the yes of section 1 for creating the registry; only later changes need their own diff and yes. An environment the user does not know yet is left out and reported, never invented;environmentsmay stay empty while onlyintegrationBranchis known. A standard one the user says the project does not have (dev, staging or prod) goes inabsentEnvironments, soharness-mapstops asking for it. - Defaults: local and dev
autonomouswith testingfull; stagingconfirm-writeswith testingnon-destructive; prodconfirm-each-stepwithallowedWithoutAsking: [],testDataAllowed: false, and testingsmoke-only. Writing a looser prod setting needs the user's explicit words. - Add
.agents/secrets.env,.agents/backups/, and.agents/deploy/to.gitignore, and create.agents/secrets.env.examplelisting everyenv:name with an empty value. Then create or top up the real.agents/secrets.envin the main checkout from it, empty values only; an existing file is never overwritten, only missing names are appended:
m="$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")"; mkdir -p "$m/.agents"; (umask 077; touch "$m/.agents/secrets.env")
[ -n "$(tail -c1 "$m/.agents/secrets.env")" ] && echo >> "$m/.agents/secrets.env" # a last line without a newline stays intact
awk -F= 'FILENAME==ARGV[1]{k=$1; sub(/^export /,"",k); have[k]=1; next} /^[A-Z][A-Z0-9_]*=/ && !($1 in have){print $1"="}' \
"$m/.agents/secrets.env" .agents/secrets.env.example >> "$m/.agents/secrets.env"In a worktree, link it as worktree-flow says (ln -s "$m/.agents/secrets.env" .agents/secrets.env). Tell the user which names to fill, and that they fill them in that file themselves, with the value or an op:// reference; never ask for a value in chat. harness-map shows each name that is still empty or absent as missing on its environment's card.
- Validate:
npx -y ajv-cli@5.0.0 validate --spec=draft2020 -s .agents/environments.schema.json -d .agents/environments.json. - A service without a version endpoint gets one per
references/version-endpoint.md, shipped as a normal PR/MR. - Run each remote service's
versionandverifyonce to prove the access works, and report anything that fails. On prod, run them only after a yes: the calling skill's batch answer (the Environments row ofharness-mapsection 3) when there is one, else ask; a no leaves the command under waiting on the user.