scripts.provision_worktree_env
Reference › scripts › provision_worktree_env
scripts/provision_worktree_env.py materialises a .env file in whichever git worktree the caller is standing in, idempotently, so a new worktree can run the code and gates that need its environment (the RUL-021 worktree-environment rule; see check_worktree_envs and worktrees). A human or a worktree-creation step runs it; it has no internal imports.
current_worktree and primary_worktree locate the worktrees with git (through _git). provision ensures that worktree/.env exists and returns whether it is present plus a short reason. It obtains the file either by decrypting the committed ciphertext (_decrypt_from_ciphertext) or by copying from the primary worktree (_copy_from_primary). Options: --worktree (default the worktree containing the current directory), --check (report only and exit non-zero when .env is absent; provisions nothing), --force (re-materialise even when one exists) and --quiet (print only on failure). Outside a git worktree it prints that there is nothing to do and exits 0.
At a glance
| Source | scripts/provision_worktree_env.py |
| Lines of code | 202 |
| Classes | 0 |
| Functions | 7 |
| Imports (internal) | none |
| Imported by | none |
| Tests that import it | none |
Class diagram
No classes; see Functions.
Dependency diagram
No internal imports and no importers.
API
Functions
| Function | Signature | Summary |
|---|---|---|
_git |
_git(**args: str, cwd: Path | None = None) -> str | None |
Run git and return stripped stdout, or None if git failed. |
current_worktree |
current_worktree(start: Path | None = None) -> Path | None |
The worktree we are standing in (its top level), or None outside a repo. |
primary_worktree |
primary_worktree(start: Path | None = None) -> Path | None |
The FIRST worktree git lists, which is always the primary checkout. |
_decrypt_from_ciphertext |
_decrypt_from_ciphertext(worktree: Path, dest: Path) -> str | None |
Try source 1: the tracked SOPS artifact. |
_copy_from_primary |
_copy_from_primary(worktree: Path, dest: Path) -> str | None |
Try source 2: the primary worktree's plaintext. |
provision |
provision(worktree: Path, *, force: bool = False) -> tuple[bool, str] |
Ensure worktree/.env exists. |
main |
main(argv: list[str] | None = None) -> int |
Source
#!/usr/bin/env python3
"""Materialise ``.env`` in whichever worktree we are standing in — idempotently.
WHY THIS EXISTS (measured 2026-08-12, session 262). A builder agent working in
``.claude/worktrees/agent-aad6ad8280c27cf26`` reported 8 test modules failing with
``SystemExit: neither KG_ROUTING_URI nor MEMGRAPH_URI is set`` and dispositioned them
as "a pre-existing environment/config gap unrelated to this packet". They were not
pre-existing: the same modules pass in the primary worktree in 8 seconds. The worktree
simply had **no ``.env`` file at all**, and the failing tests read the ``.env`` FILE
from disk (``dotenv_values(_ENV_FILE)``) rather than ``os.environ`` — so a fully
populated shell environment did not help them.
That is the expensive shape of this defect: it does not present as "missing config",
it presents as **product tests failing**, which invites a wrong diagnosis and burns an
agent's whole budget chasing it. ``scripts/check_worktree_envs.py`` gated ``.venv`` and
never looked for ``.env``, so RUL-021 ("every damn feature branch gets a full env") was
enforced for half the env.
WHY A SEPARATE SCRIPT rather than logic inside a hook: worktrees are created by at
least two paths that do not share code — ``scripts/worktrees.py`` (the project's own
lifecycle manager) and the Claude Code harness's ``isolation: worktree`` (which never
calls project code). The only thing BOTH paths run is git's ``post-checkout`` hook,
which ``git worktree add`` fires in the new worktree (verified empirically, not
assumed). Keeping the logic here means the hook is a one-line trigger and every other
caller — provisioner, gate, CI, a human — invokes the identical code path.
SOURCE PRECEDENCE, most trustworthy first:
1. Decrypt the tracked SOPS artifact, when an age identity is available. This is the
real answer: the ciphertext travels with the repo, so a worktree (or a fresh
clone) reconstructs its own secrets.
2. Copy the primary worktree's ``.env``. Same machine, same user, same secrets that
are already on this disk — this is a convenience, not a security boundary, and it
is what makes worktrees work TODAY on a machine whose key is already present.
3. Neither available -> report and EXIT 0.
**Never blocks a checkout.** A hook that can fail a ``git worktree add`` or a branch
switch converts a missing convenience into a broken repository. Every failure path here
is a printed line and exit 0; only ``--check`` reports absence as non-zero, because a
GATE is exactly the place where absence should be loud.
"""
from __future__ import annotations
import argparse
import os
import shutil
import subprocess
import sys
from pathlib import Path
ENV_FILENAME = ".env"
CIPHERTEXT_RELPATH = Path("secrets") / "env.sops.env"
#: Exit codes. ``--check`` is the only mode that reports a missing ``.env`` as a
#: failure; the hook path is always 0 (see the module docstring).
OK = 0
MISSING = 1
def _git(*args: str, cwd: Path | None = None) -> str | None:
"""Run git and return stripped stdout, or None if git failed."""
try:
done = subprocess.run(
["git", *args],
cwd=str(cwd) if cwd else None,
capture_output=True,
text=True,
check=True,
)
except subprocess.CalledProcessError, OSError:
return None
return done.stdout.strip()
def current_worktree(start: Path | None = None) -> Path | None:
"""The worktree we are standing in (its top level), or None outside a repo."""
top = _git("rev-parse", "--show-toplevel", cwd=start)
return Path(top) if top else None
def primary_worktree(start: Path | None = None) -> Path | None:
"""The FIRST worktree git lists, which is always the primary checkout.
``git worktree list --porcelain`` emits the primary first; every linked worktree
follows. Deriving it this way (rather than assuming a path shape like
``.claude/worktrees/*``) keeps this correct for worktrees created anywhere.
"""
listing = _git("worktree", "list", "--porcelain", cwd=start)
if not listing:
return None
for line in listing.splitlines():
if line.startswith("worktree "):
return Path(line[len("worktree ") :].strip())
return None
def _decrypt_from_ciphertext(worktree: Path, dest: Path) -> str | None:
"""Try source 1: the tracked SOPS artifact. Returns a description on success."""
ciphertext = worktree / CIPHERTEXT_RELPATH
if not ciphertext.is_file():
return None
secrets_cli = worktree / "scripts" / "secrets_cli.py"
if not secrets_cli.is_file():
return None
try:
done = subprocess.run(
[sys.executable, str(secrets_cli), "decrypt", "--dest", str(dest)],
cwd=str(worktree),
capture_output=True,
text=True,
)
except OSError:
return None
if done.returncode != OK or not dest.is_file():
return None
return f"decrypted from {CIPHERTEXT_RELPATH}"
def _copy_from_primary(worktree: Path, dest: Path) -> str | None:
"""Try source 2: the primary worktree's plaintext. Returns a description."""
primary = primary_worktree(worktree)
if primary is None or primary.resolve() == worktree.resolve():
return None
source = primary / ENV_FILENAME
if not source.is_file():
return None
# Copy through a temp file + replace so an interrupted copy cannot leave a
# truncated .env that would fail in a far more confusing way than an absent one.
tmp = dest.with_name(dest.name + ".provision.tmp")
try:
shutil.copyfile(source, tmp)
os.chmod(tmp, 0o600)
os.replace(tmp, dest)
except OSError:
tmp.unlink(missing_ok=True)
return None
return f"copied from the primary worktree ({primary})"
def provision(worktree: Path, *, force: bool = False) -> tuple[bool, str]:
"""Ensure ``worktree/.env`` exists. Returns (present, human-readable reason)."""
dest = worktree / ENV_FILENAME
if dest.is_file() and not force:
return True, "already present"
for attempt in (_decrypt_from_ciphertext, _copy_from_primary):
detail = attempt(worktree, dest)
if detail is not None:
return True, detail
return False, (
"no source available — no decryptable secrets/env.sops.env and no readable "
".env in the primary worktree"
)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--worktree",
default=None,
help="Worktree to provision (default: the one containing the cwd)",
)
parser.add_argument(
"--check",
action="store_true",
help="Report only; exit non-zero when .env is absent. Provisions nothing.",
)
parser.add_argument(
"--force",
action="store_true",
help="Re-materialise even when a .env is already present",
)
parser.add_argument("--quiet", action="store_true", help="Print only on failure")
args = parser.parse_args(argv)
start = Path(args.worktree).resolve() if args.worktree else None
worktree = current_worktree(start) if start is None else start
if worktree is None:
print("[worktree-env] not a git worktree — nothing to do")
return OK
if args.check:
present = (worktree / ENV_FILENAME).is_file()
state = "present" if present else "MISSING"
print(f"[worktree-env] {worktree}: .env {state}")
return OK if present else MISSING
present, reason = provision(worktree, force=args.force)
if present:
if not args.quiet or reason != "already present":
print(f"[worktree-env] {worktree}: .env {reason}")
return OK
# Deliberately exit 0: see the module docstring. A checkout must never fail
# because a convenience file could not be created.
print(f"[worktree-env] {worktree}: .env NOT created — {reason}", file=sys.stderr)
return OK
if __name__ == "__main__":
raise SystemExit(main())