Shadow Testing Skill
Purpose [LEVEL 1]
Shadow testing creates isolated container environments where you can test local uncommitted changes without affecting your host system or pushing to remote repositories.
Key Principle: Test exactly what's on your machine (including uncommitted changes) in a clean, isolated environment that mirrors CI.
When to Use This Skill [LEVEL 1]
Perfect For
-
Pre-Push Validation: Test changes before committing/pushing
-
Multi-Repo Coordination: Validate changes across multiple repositories work together
-
Clean-State Testing: "Does it work on a fresh machine?"
-
Library Development: Test library changes with dependent projects
-
CI Parity: See what CI will see before pushing
-
Destructive Testing: Tests that modify system state won't affect host
Use This Skill When
-
Making breaking changes to a library others depend on
-
Coordinating changes across multiple repositories
-
Unsure if your changes will work in CI
-
Need to test with specific dependency versions
-
Want to verify install/setup procedures work
-
Testing changes that require clean environment state
Don't Use This Skill When
-
Running unit tests on already-committed code (use local test runner)
-
Need to debug with live code changes (shadow captures snapshots)
-
Testing production deployment (use staging environments)
-
Simple single-file changes with good test coverage
Core Concepts [LEVEL 1]
Shadow Environment Architecture
A shadow environment is a Docker/Podman container with:
-
Git Bundle Snapshots - Exact working tree state (including uncommitted changes)
-
Embedded Gitea Server - Local git server at localhost:3000 inside container
-
Selective URL Rewriting - Git insteadOf rules redirect specific repos to local Gitea
-
Package Manager Isolation - UV, pip, npm, cargo, go caches isolated per shadow
-
API Key Passthrough - Common API keys automatically forwarded to container
┌─────────────────────────────────────────────────────────┐ │ Shadow Container │ │ ┌───────────────────────────────────────────────────┐ │ │ │ Gitea Server (localhost:3000) │ │ │ │ - myorg/my-library (your snapshot) │ │ │ └───────────────────────────────────────────────────┘ │ │ │ │ Git URL Rewriting: │ │ github.com/myorg/my-library → Gitea (local) │ │ github.com/myorg/other-repo → Real GitHub │ │ │ │ /workspace (pre-cloned local sources) │ └─────────────────────────────────────────────────────────┘
How Git URL Rewriting Works
When you create a shadow with ~/repos/my-lib:myorg/my-lib :
-
Your working directory is captured exactly as-is (uncommitted changes included)
-
Snapshot is bundled with full git history
-
Container starts with Gitea server
-
Snapshot pushed to Gitea as myorg/my-lib
-
Git config adds insteadOf rules: [url "http://shadow:shadow@localhost:3000/myorg/my-lib.git"] insteadOf = https://github.com/myorg/my-lib.git
-
Any git clone https://github.com/myorg/my-lib → uses YOUR local snapshot
-
All other GitHub URLs → fetch from real GitHub
Result: Only your specified repos are local; everything else uses production sources.
Quick Start [LEVEL 1]
Installation
For Amplifier Users (native integration):
Shadow tool is built-in - no installation needed
amplifier run --bundle amplihack
For Other Agents (standalone CLI):
Install via uvx (recommended)
uvx amplifier-shadow --version
Or via pip
pip install amplifier-bundle-shadow
Verify installation
amplifier-shadow --version
Prerequisites:
-
Docker or Podman installed and running
-
Git installed
Your First Shadow (CLI)
Create shadow with your local library changes
amplifier-shadow create --local ~/repos/my-library:myorg/my-library --name test-lib
Inside the shadow, install via git URL
→ my-library uses YOUR LOCAL snapshot
→ all other dependencies fetch from REAL GitHub
amplifier-shadow exec test-lib "uv pip install git+https://github.com/myorg/my-library"
Run tests
amplifier-shadow exec test-lib "cd /workspace && pytest"
See what changed
amplifier-shadow diff test-lib
Clean up when done
amplifier-shadow destroy test-lib
Your First Shadow (Amplifier Tool)
Create shadow with local changes
shadow.create(local_sources=["~/repos/my-library:myorg/my-library"])
Execute commands
shadow.exec(shadow_id, "uv pip install git+https://github.com/myorg/my-library") shadow.exec(shadow_id, "pytest tests/")
Extract results
shadow.extract(shadow_id, "/workspace/test-results", "./results")
Cleanup
shadow.destroy(shadow_id)
Tool Reference by Agent Type [LEVEL 2]
Amplifier (Native Integration)
Best experience - shadow is a first-class tool with automatic setup:
All operations via shadow tool
result = shadow.create( local_sources=["~/repos/lib:org/lib"], verify=True # Automatic smoke test )
Integrated error handling and observability
if result.ready: shadow.exec(result.shadow_id, "pytest")
Features:
-
Automatic API key passthrough
-
Built-in smoke tests and health checks
-
Integrated with other Amplifier tools
-
Session-aware cleanup
Claude Code Standalone
Use the CLI directly from bash tool:
All operations via amplifier-shadow CLI
uvx amplifier-shadow create --local ~/repos/my-lib:org/my-lib --name test
uvx amplifier-shadow exec test "pip install -e /workspace/org/my-lib" uvx amplifier-shadow exec test "pytest"
uvx amplifier-shadow destroy test
GitHub Copilot
Same CLI interface as Claude Code:
Install once
pip install amplifier-bundle-shadow
Use in workflow
amplifier-shadow create --local ~/repos/lib:org/lib amplifier-shadow exec shadow-xxx "npm install && npm test"
Manual/DIY (Any Agent)
Use the provided shell scripts and Docker Compose examples (see Level 3).
Common Patterns [LEVEL 2]
Pattern: Test Library Changes Before Publishing
Test your library with its dependents
amplifier-shadow create --local ~/repos/my-library:myorg/my-library --name lib-test
Clone dependent project and install
amplifier-shadow exec lib-test " cd /workspace && git clone https://github.com/myorg/dependent-app && cd dependent-app && uv venv && . .venv/bin/activate && uv pip install git+https://github.com/myorg/my-library && pytest "
Pattern: Multi-Repo Changes
Testing changes across multiple repos
amplifier-shadow create
--local ~/repos/core-lib:myorg/core-lib
--local ~/repos/cli-tool:myorg/cli-tool
--name multi-test
Both local sources will be used
amplifier-shadow exec multi-test "uv pip install git+https://github.com/myorg/cli-tool"
Pattern: Iterate on Failures
1. Create shadow and run tests
amplifier-shadow create --local ~/repos/lib:org/lib --name test amplifier-shadow exec test "pytest" # Fails
2. Fix code locally on host
3. Destroy and recreate (picks up your local changes)
amplifier-shadow destroy test amplifier-shadow create --local ~/repos/lib:org/lib --name test amplifier-shadow exec test "pytest" # Passes
4. Commit with confidence!
git commit -m "Fix issue"
Pattern: Pre-Push CI Validation
Run your CI script in shadow before pushing
amplifier-shadow create --local ~/repos/project:org/project --name ci-check
amplifier-shadow exec ci-check " cd /workspace/org/project && ./scripts/ci.sh "
If CI script passes, your push will likely succeed
Verification Best Practices [LEVEL 2]
Always Verify Local Sources Are Used
After creating a shadow, confirm your local code is actually being used:
Step 1: Check snapshot commits (from create output)
amplifier-shadow create --local ~/repos/lib:org/lib
Output shows: snapshot_commits: {"org/lib": "abc1234..."}
Step 2: Compare with install output
amplifier-shadow exec shadow-xxx "uv pip install git+https://github.com/org/lib"
Look for: lib @ git+...@abc1234
If commits match, your local code is being used!
Pre-Cloned Repository Locations
Local sources are automatically cloned to /workspace/{org}/{repo} :
Your local source microsoft/my-library is available at:
/workspace/microsoft/my-library
Use for editable installs (Python)
amplifier-shadow exec shadow-xxx "pip install -e /workspace/microsoft/my-library"
Or for Node.js
amplifier-shadow exec shadow-xxx "cd /workspace/microsoft/my-package && npm install"
Always check this location first - the repo is already there.
Environment Variable Verification
Don't assume - verify API keys are present!
amplifier-shadow exec shadow-xxx "env | grep API_KEY"
Check all passed variables
amplifier-shadow status shadow-xxx
Shows: env_vars_passed: ["ANTHROPIC_API_KEY", ...]
Troubleshooting [LEVEL 2]
Common Issues
"UV tool install" uses cache instead of local source:
Problem: UV may bypass git URL rewriting for cached packages.
Solution:
Option 1: Install from pre-cloned workspace (recommended)
amplifier-shadow exec xxx "pip install -e /workspace/org/lib"
Option 2: Clear UV cache first
amplifier-shadow exec xxx "rm -rf /tmp/uv-cache && uv tool install git+https://github.com/org/lib"
"PEP 668: Externally-Managed Environment":
Solution: Always use virtual environments inside shadow:
amplifier-shadow exec xxx " cd /workspace && uv venv && . .venv/bin/activate && uv pip install ... "
"Container image not found":
Solution: Build the image locally:
amplifier-shadow build
"/workspace permission denied":
Solution: Use $HOME or /tmp as alternatives:
amplifier-shadow exec xxx "cd $HOME && git clone ..."
Level 3: Advanced Topics [LEVEL 3]
Custom Docker Images
Build your own shadow image with additional tools:
FROM ghcr.io/microsoft/amplifier-shadow:latest
Add your tools
RUN apt-get update && apt-get install -y
postgresql-client
redis-tools
Add custom scripts
COPY my-test-script.sh /usr/local/bin/
Build and use:
docker build -t my-shadow:latest . amplifier-shadow create --image my-shadow:latest --local ~/repos/lib:org/lib
Shell Scripts (DIY Shadow Setup)
For agents without Amplifier access, use these standalone scripts:
Script 1: Create Git Bundle (scripts/create-bundle.sh ):
#!/bin/bash
Create git bundle snapshot of working tree
REPO_PATH=$1 OUTPUT_PATH=$2
cd "$REPO_PATH"
Fetch all refs to ensure complete history
git fetch --all --tags --quiet 2>/dev/null || true
Check for uncommitted changes
if [[ -n $(git status --porcelain) ]]; then # Create temp clone and commit changes TEMP_DIR=$(mktemp -d) git clone --quiet "$REPO_PATH" "$TEMP_DIR"
# Sync working tree (including deletions)
rsync -a --delete --exclude='.git' "$REPO_PATH/" "$TEMP_DIR/"
cd "$TEMP_DIR"
git add -A
git commit --allow-empty -m "Shadow snapshot" --author="Shadow <shadow@localhost>"
# Create bundle
git bundle create "$OUTPUT_PATH" --all
cd /
rm -rf "$TEMP_DIR"
else # Clean repo - just bundle it git bundle create "$OUTPUT_PATH" --all fi
echo "Bundle created: $OUTPUT_PATH"
Script 2: Setup Shadow Container (scripts/setup-shadow.sh ):
#!/bin/bash
Start container with Gitea and configure git URL rewriting
CONTAINER_NAME=$1 BUNDLE_PATH=$2 ORG=$3 REPO=$4
Start container
docker run -d
--name "$CONTAINER_NAME"
-v "$BUNDLE_PATH:/snapshots/bundle.git:ro"
ghcr.io/microsoft/amplifier-shadow:latest
Wait for Gitea
echo "Waiting for Gitea to start..." until docker exec "$CONTAINER_NAME" curl -sf http://localhost:3000/api/v1/version > /dev/null; do sleep 1 done
Create org and repo in Gitea
docker exec "$CONTAINER_NAME" bash -c "
curl -s -u shadow:shadow
-H 'Content-Type: application/json'
-d '{"username":"$ORG"}'
http://localhost:3000/api/v1/orgs
curl -s -u shadow:shadow \
-H 'Content-Type: application/json' \
-d '{\"name\":\"$REPO\",\"private\":false}' \
http://localhost:3000/api/v1/orgs/$ORG/repos
"
Push bundle to Gitea
docker exec "$CONTAINER_NAME" bash -c " cd /tmp && git init --bare repo.git && cd repo.git && git fetch /snapshots/bundle.git refs/heads/:refs/heads/ && git remote add origin http://shadow:shadow@localhost:3000/$ORG/$REPO.git && git push origin --all --force "
Configure git URL rewriting
docker exec "$CONTAINER_NAME" bash -c " git config --global url.'http://shadow:shadow@localhost:3000/$ORG/$REPO.git'.insteadOf 'https://github.com/$ORG/$REPO.git' "
echo "Shadow container ready: $CONTAINER_NAME" echo "Local source: $ORG/$REPO"
Usage:
Create bundle from your repo
./scripts/create-bundle.sh ~/repos/my-lib /tmp/my-lib.bundle
Setup shadow container
./scripts/setup-shadow.sh shadow-test /tmp/my-lib.bundle myorg my-lib
Test
docker exec shadow-test bash -c " git clone https://github.com/myorg/my-lib /tmp/test && cd /tmp/test && git log -1 --oneline "
Docker Compose Examples
Example 1: Single Repository (docker-compose/single-repo.yml ):
version: "3.8"
services: shadow: image: ghcr.io/microsoft/amplifier-shadow:latest container_name: shadow-single volumes: - ./snapshots:/snapshots:ro - ./workspace:/workspace environment: - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - OPENAI_API_KEY=${OPENAI_API_KEY} command: > bash -c " /usr/local/bin/gitea-init.sh && tail -f /dev/null "
Example 2: Multi-Repository Testing (docker-compose/multi-repo.yml ):
version: "3.8"
services: shadow-multi: image: ghcr.io/microsoft/amplifier-shadow:latest container_name: shadow-multi volumes: # Mount multiple bundles - ./snapshots/core-lib.bundle:/snapshots/org/core-lib.bundle:ro - ./snapshots/cli-tool.bundle:/snapshots/org/cli-tool.bundle:ro - ./workspace:/workspace environment: # Pass API keys from host - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} # UV cache isolation - UV_CACHE_DIR=/tmp/uv-cache command: > bash -c " /usr/local/bin/gitea-init.sh && /usr/local/bin/setup-repos.sh org/core-lib org/cli-tool && tail -f /dev/null "
Usage:
Create bundles for your repos
git -C ~/repos/core-lib bundle create snapshots/core-lib.bundle --all git -C ~/repos/cli-tool bundle create snapshots/cli-tool.bundle --all
Start shadow
docker-compose -f docker-compose/multi-repo.yml up -d
Run tests
docker-compose exec shadow-multi bash -c " cd /workspace && git clone https://github.com/org/cli-tool && cd cli-tool && uv pip install -e . pytest "
Cleanup
docker-compose down
Example 3: CI Integration (docker-compose/ci-shadow.yml ):
version: "3.8"
services: ci-shadow: image: ghcr.io/microsoft/amplifier-shadow:latest container_name: ci-shadow volumes: - ./snapshots:/snapshots:ro - ./test-results:/test-results environment: - CI=true - GITHUB_ACTIONS=true command: > bash -c " /usr/local/bin/gitea-init.sh && /usr/local/bin/run-ci-tests.sh > /test-results/output.log 2>&1 "
GitHub Actions Integration:
.github/workflows/shadow-test.yml
name: Shadow Test
on: [push, pull_request]
jobs: shadow-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3
- name: Create git bundle
run: git bundle create snapshot.bundle --all
- name: Run shadow tests
run: |
docker run --rm \
-v $PWD/snapshot.bundle:/snapshots/bundle.git:ro \
ghcr.io/microsoft/amplifier-shadow:latest \
/usr/local/bin/test-in-shadow.sh org/repo
Integration with Outside-In Testing
Combine shadow environments with agentic outside-in tests:
Create shadow with local changes
amplifier-shadow create --local ~/repos/lib:org/lib --name test
Run outside-in test scenarios inside shadow
amplifier-shadow exec test "gadugi-agentic-test run test-scenario.yaml"
Extract evidence
amplifier-shadow extract test /evidence ./test-evidence
See the qa-team skill for complete integration examples (outside-in-testing remains an alias).
Best Practices [LEVEL 2]
- Always Verify Your Sources Are Used
Don't assume - verify that the shadow is actually using your local code:
Check snapshot commits
amplifier-shadow status shadow-xxx | grep snapshot_commit
Verify install resolves to that commit
amplifier-shadow exec shadow-xxx "pip install git+https://github.com/org/lib" | grep "org/lib @"
- Use Pre-Cloned Workspace
Local sources are automatically at /workspace/{org}/{repo} :
✅ FAST: Use pre-cloned repo
amplifier-shadow exec xxx "pip install -e /workspace/org/lib"
❌ SLOWER: Clone again
amplifier-shadow exec xxx "git clone https://github.com/org/lib && pip install -e lib"
- Isolate Package Manager Caches
Shadow environments automatically isolate caches to prevent stale packages:
-
Python UV: /tmp/uv-cache
-
Python pip: /tmp/pip-cache
-
Node npm: /tmp/npm-cache
-
Rust cargo: /tmp/cargo-home
-
Go modules: /tmp/go-mod-cache
These are set automatically - no action needed.
- Pass Required Environment Variables
Amplifier (automatic for common API keys)
shadow.create(local_sources=["~/repos/lib:org/lib"])
CLI (explicit)
amplifier-shadow create
--local ~/repos/lib:org/lib
--env ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY
--env CUSTOM_VAR=value
- Clean Up After Testing
Always destroy shadows when done
amplifier-shadow destroy shadow-xxx
Or destroy all
amplifier-shadow destroy-all
- Use Named Shadows for Clarity
✅ GOOD: Descriptive name
amplifier-shadow create --local ~/repos/lib:org/lib --name test-breaking-change
❌ BAD: Auto-generated
amplifier-shadow create --local ~/repos/lib:org/lib
Creates shadow-a3f2b8c1 (hard to remember)
Integration Patterns [LEVEL 3]
Pattern: Shadow + Outside-In Tests
Combine shadow isolation with declarative test scenarios:
test-scenario.yaml
scenario: name: "Library Integration Test" type: cli
steps: - action: launch target: "/workspace/org/lib/cli.py"
- action: verify_output
contains: "Success"
Run in shadow:
amplifier-shadow create --local ~/repos/lib:org/lib --name test amplifier-shadow exec test "gadugi-agentic-test run test-scenario.yaml"
Pattern: Shadow + pytest
amplifier-shadow create --local ~/repos/lib:org/lib --name pytest-run
amplifier-shadow exec pytest-run " cd /workspace/org/lib && uv venv && . .venv/bin/activate && pip install -e '.[dev]' && pytest --cov=src --cov-report=html "
Extract coverage report
amplifier-shadow extract pytest-run /workspace/org/lib/htmlcov ./coverage-report
Pattern: Shadow + npm test
amplifier-shadow create --local ~/repos/pkg:org/pkg --name npm-test
amplifier-shadow exec npm-test " cd /workspace/org/pkg && npm install && npm test "
Pattern: Shadow + cargo test
amplifier-shadow create --local ~/repos/crate:org/crate --name cargo-test
amplifier-shadow exec cargo-test " cd /workspace/org/crate && cargo build && cargo test "
Philosophy Alignment [LEVEL 2]
This skill follows amplihack's core principles:
Ruthless Simplicity
-
Minimal abstraction: Shadow = container + gitea + URL rewriting
-
No frameworks: Pure Docker, git, and shell scripts
-
Essential only: Only captures what's needed (git bundle, not entire filesystems)
Modular Design (Bricks & Studs)
-
Self-contained: Each shadow is independent
-
Clear contract: Git URLs in → local sources out
-
Composable: Combine with other testing tools
Zero-BS Implementation
-
No stubs: Every script works completely
-
Working defaults: Reasonable defaults for all operations
-
Clear errors: Actionable error messages with troubleshooting
Outside-In Thinking
-
User perspective: Test what users will see
-
Implementation agnostic: Don't care how code works internally
-
Behavior-driven: Focus on outcomes
CLI Reference [LEVEL 3]
Commands
Create shadow environment
amplifier-shadow create [OPTIONS] --local, -l TEXT Local source mapping: /path/to/repo:org/name (repeatable) --name, -n TEXT Name for environment (auto-generated if not provided) --image, -i TEXT Container image (default: amplifier-shadow:local) --env, -e TEXT Environment variable: KEY=VALUE or KEY to inherit (repeatable) --env-file FILE File with environment variables (one per line) --pass-api-keys Auto-pass common API key env vars (default: enabled)
Execute command in shadow
amplifier-shadow exec SHADOW_ID COMMAND --timeout INTEGER Timeout in seconds (default: 300)
Show changed files
amplifier-shadow diff SHADOW_ID [PATH]
Extract file from shadow
amplifier-shadow extract SHADOW_ID CONTAINER_PATH HOST_PATH
Inject file into shadow
amplifier-shadow inject SHADOW_ID HOST_PATH CONTAINER_PATH
List all shadows
amplifier-shadow list
Show shadow status
amplifier-shadow status SHADOW_ID
Destroy shadow
amplifier-shadow destroy SHADOW_ID --force Force destruction even on errors
Destroy all shadows
amplifier-shadow destroy-all --force Force destruction even on errors
Build shadow image locally
amplifier-shadow build
Open interactive shell
amplifier-shadow shell SHADOW_ID
Quick Reference Card [LEVEL 1]
Typical workflow
amplifier-shadow create --local ~/repos/lib:org/lib --name test amplifier-shadow exec test "pytest" amplifier-shadow destroy test
Multi-repo
amplifier-shadow create
--local ~/repos/lib1:org/lib1
--local ~/repos/lib2:org/lib2
--name multi
With environment variables
amplifier-shadow create
--local ~/repos/lib:org/lib
--env API_KEY=$API_KEY
--name test
Interactive shell
amplifier-shadow shell test
Extract results
amplifier-shadow extract test /workspace/results ./local-results
Related Skills [LEVEL 1]
-
qa-team: Run agentic tests in shadow environments (legacy name: outside-in-testing )
-
test-gap-analyzer: Find untested code paths (complement shadow testing)
-
philosophy-guardian: Verify shadow scripts follow ruthless simplicity
Troubleshooting Checklist [LEVEL 2]
When shadow tests fail:
-
Verify local sources are being used (check snapshot commits)
-
Check pre-cloned repos exist at /workspace/{org}/{repo}
-
Verify environment variables are passed (run env inside shadow)
-
Clear package manager caches if stale
-
Check git URL rewriting is configured (git config --list )
-
Verify Gitea is accessible (curl http://localhost:3000/api/v1/version )
-
Use virtual environments (avoid PEP 668 errors)
-
Check container is running (amplifier-shadow status )
Changelog [LEVEL 3]
Version 1.0.0 (2026-01-29)
-
Initial skill release
-
Support for Amplifier, Claude Code, GitHub Copilot, manual DIY
-
Shell scripts for standalone usage
-
Docker Compose examples for CI integration
-
Complete CLI reference and troubleshooting guide
-
Integration patterns with qa-team / outside-in-testing alias
-
Philosophy alignment with ruthless simplicity
Remember: Shadow environments let you test exactly what's on your machine (uncommitted changes and all) in a clean, isolated environment that mirrors CI. Use them before every significant push to catch issues early.