Isolated Dev Environments (Shadow Clones)
Working on spec-kitty often means running the CLI against a checkout while you are editing that same checkout's source. When you keep more than one checkout on a machine — a stable primary plus one or more experiment branches — two of them can quietly interfere with each other. This guide sets up a Shadow Clone: a standalone spec-kitty checkout whose CLI and runtime state are pinned to itself, so nothing it does leaks into the machine-global install or into sibling clones.
This is the non-containerised option. It is chosen deliberately for speed and low overhead; a container gives stronger isolation but costs startup time and disk on every invocation. If you need hard OS-level isolation, containerise instead — this guide is for the fast, everyday case.
The two things that actually leak
Isolation has exactly two axes. Miss either one and the clones are still entangled.
| Axis | What leaks if unisolated | The lever |
|---|---|---|
Code — which spec-kitty runs |
spec-kitty on your PATH resolves to the machine-global binary, so it runs global code against the clone's files. Edits to the clone's src/ have no effect until the global install is rebuilt. |
A clone-local .venv (editable install) placed first on PATH. |
| State — where the CLI reads/writes | With SPEC_KITTY_HOME unset, every clone shares one runtime-state root at ~/.spec-kitty — the offline queue (queue.db), the sync daemon, the event journal, auth tokens, gate-locks, and trackers. One clone's daemon and queue then act on another clone's work. |
Point SPEC_KITTY_HOME at a clone-local directory. |
SPEC_KITTY_HOME is the single environment variable that redirects the runtime
state. When set, it is used verbatim as the state root (it is not suffixed
with .spec-kitty), and it redirects both resolvers the codebase relies on —
get_runtime_root() (runtime state) and get_kittify_home() (asset home). See
src/kernel/paths.py and
src/specify_cli/paths/windows_paths.py.
The parallel test suite isolates itself the same way — each
pytest-xdistworker gets its ownHOME/SPEC_KITTY_HOMEso a run never touches the real~/.spec-kitty. A Shadow Clone applies that discipline to interactive sessions.
Mental model: primary vs. shadow
- Primary checkout — your stable clone. Its build backs the
machine-global
spec-kittyyou use everywhere outside a Shadow Clone. When no Shadow Clone is active, the machine-global CLI is what runs. - Shadow Clone — any additional isolated checkout. Inside its session, a
clone-local
.venvand a clone-local state root take over; outside, nothing changed.
The rest of this guide keeps those two intact at the same time: the global CLI stays the default everywhere, and each Shadow Clone overrides it only for the shell session you activate.
One-time machine setup (the global CLI)
Do this once, from your primary checkout, so that spec-kitty works from
any directory that is not an activated Shadow Clone. Two supported shapes:
A. Snapshot from source (recommended default). Install the primary
checkout's build into an isolated app environment with
pipx. This gives you a global spec-kitty that is
your fork's code rather than the published PyPI release, without putting the
primary checkout's .venv on your PATH:
# from the primary checkout root
pipx install --force .
Re-run that command whenever you want the global CLI to pick up new primary- checkout changes. It is a snapshot, not a live link: editing the primary checkout's source does not change the global CLI until you reinstall.
B. Live editable link. If you want the global CLI to always track the primary checkout's working tree, install it editable into a dedicated environment instead:
# from the primary checkout root
pipx install --force --editable .
Either way, confirm the global resolves as expected from a neutral directory:
cd ~ # anywhere that is not a Shadow Clone
command -v spec-kitty # -> ~/.local/bin/spec-kitty (the pipx shim)
spec-kitty --version
Keep the primary checkout's own
.venvfor running its tests, but do not add it to your shell's defaultPATH. The global shim is the machine default; the primary.venvis just how you test the primary checkout.
Per-clone setup (each Shadow Clone)
For every additional checkout you want to isolate:
1. Create the clone-local virtualenv (editable install + dev extras)
Use the project's canonical install — the same command
contributing.md prescribes. From the clone root it creates
.venv, installs the clone editable (so the CLI always runs the clone's live
src/), and pulls in the dev extras — the test and lint optional-dependency
groups (pytest, pytest-xdist, ruff, mypy, …) plus the dev dependency
group — resolved from the committed uv.lock:
# from the Shadow Clone root
uv sync --frozen --all-extras
Do not skip the dev extras: a Shadow Clone you cannot run the test suite,
ruff, or mypy in is a half-isolated clone that still forces you back to
another checkout to validate a change. uv sync installs them by default here,
so a correctly set-up clone is self-sufficient.
No
uv? Install it (https://docs.astral.sh/uv/), or fall back to the stdlib path — but install the extras explicitly so the clone stays self-sufficient:python3.11 -m venv .venv # match the project's supported Python (3.11+) .venv/bin/python -m pip install -e '.[test,lint]'Playwright's browser (for
tests/ui/) is never committed; install it once per clone when you need the UI e2e tests:.venv/bin/playwright install chromium.
Verify the clone-local CLI and dev tools run the clone's code:
.venv/bin/spec-kitty --version
.venv/bin/pytest --version && .venv/bin/ruff --version && .venv/bin/mypy --version
.venv/ is already git-ignored, so this never becomes a committed artefact.
2. Activate the isolated environment for your session
A committed, path-agnostic helper does both axes at once — prepends the clone's
.venv/bin to PATH and points SPEC_KITTY_HOME at a clone-local state root.
Source it (do not execute it); it works from both bash and zsh:
# from the Shadow Clone root
source scripts/dev/activate-isolated-env.sh
It prints what it bound and how to undo it:
spec-kitty isolated env ACTIVE
clone : <clone-root>
venv : <clone-root>/.venv
state : <clone-root>/.spec-kitty-home
cli : <clone-root>/.venv/bin/spec-kitty
undo : deactivate_spec_kitty
The helper derives the clone root from its own location, so the identical file
works in every clone with no per-machine edits. The clone-local state root
(.spec-kitty-home/) is git-ignored.
To restore the shell to the machine-global CLI and unset the overrides:
deactivate_spec_kitty
3. Verify the isolation
With the environment active:
command -v spec-kitty # -> <clone-root>/.venv/bin/spec-kitty
echo "$SPEC_KITTY_HOME" # -> <clone-root>/.spec-kitty-home
spec-kitty agent tasks status # any command now writes only under .spec-kitty-home/
ls .spec-kitty-home # clone-local queue.db / sync / journal appear here
Confirm the machine-global root is untouched — nothing new should appear under
~/.spec-kitty as a result of commands run inside the Shadow Clone.
Optional: auto-activate with direnv
Sourcing by hand is reliable but easy to forget. If you use
direnv, drop an .envrc at each clone root so the
environment activates on cd and deactivates when you leave:
# <clone-root>/.envrc
source scripts/dev/activate-isolated-env.sh
Then direnv allow once per clone. direnv is optional and is not a
project dependency; the sourced helper remains the baseline that needs nothing
installed. If you commit an .envrc, keep it path-agnostic (as above) so it
works in any clone.
What is (and is not) isolated
Isolated per clone once activated:
- Which
spec-kittybinary runs (clone.venv, editable → livesrc/). - Runtime state under
SPEC_KITTY_HOME: offline queue, sync daemon, event journal, auth, gate-locks, trackers, config.
Deliberately not isolated:
- The machine-global
spec-kitty— it stays your default everywhere outside an activated Shadow Clone. Setting up a Shadow Clone never rewrites it. - Anything a command reaches over the network (a hosted SaaS/tracker backend is shared infrastructure regardless of local state). Isolation here is about local runtime state, not remote services.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
spec-kitty still resolves to ~/.local/bin/... after activating |
The helper was executed, not sourced, so the exports never reached your shell. | source scripts/dev/activate-isolated-env.sh (note the leading source). |
no .venv found at <root> |
The clone-local virtualenv was not created yet. | Run step 1 (create .venv + pip install -e .). |
Edits to the clone's src/ have no effect |
The active CLI is the machine-global one, or the .venv install is not editable. |
Activate the env; reinstall with pip install -e .. |
| A sync daemon or queue seems to act on another clone's work | SPEC_KITTY_HOME was unset, so the shared ~/.spec-kitty was in play. |
Activate the env; confirm echo "$SPEC_KITTY_HOME" points inside the clone. |
State appeared under ~/.spec-kitty while working in a clone |
A command ran before activation. | Deactivate/reactivate; run clone commands only inside an activated session. |
Related
- Local overrides for cross-package development — editable
installs of sibling packages (
-events/-tracker) and why committed[tool.uv.sources]path overrides are prohibited. - Contributing to Spec Kitty — developer setup and the test workflow.
- Glossary: Shadow Clone (Isolated Dev Environment) in
docs/context/execution.md.