Cross-cloud topology
runtimeIPSec/IKEv2 tunnel between Azure VPN Gateway (eastus2) and AWS VGW (us-east-1) with BGP inside the tunnel. Single tunnel attached today; AWS hands out two by default and the second is staged but not wired. Inside CIDR 169.254.21.0/30 is locked because Azure's local_network_gateway needs the AWS-side BGP peer IP at apply time.
Repo layout & two-roots model
codeEach cloud is its own Terraform root with local state. The two share no backend — the seam between them is a manual outputs-as-inputs paste, formalized as the three-phase apply (§6). This is what lets two parallel Claude sessions (one per worktree) work the lab without colliding on plan/apply.
CLAUDE.md
agents/
hooks/
settings.json"] GH[".github/
FUNDING · PR template"] HOOKS[".githooks/pre-push
(activated by scripts/install-git-hooks.sh)"] AWS_TF["aws/labs/vpn-site-to-site/
main.tf · variables.tf
outputs.tf · destroy.sh"] AZ_TF["azure/labs/vpn-site-to-site/
main.tf · variables.tf
outputs.tf · destroy.sh"] DOCS["docs/
wiki/
reference-architectures/
index.html"] REFARCH["docs/reference-architectures/
aws/ · azure/
cross-cloud/ · claude/"] TEAR["teardown-all.sh
(walks labs/, runs each destroy.sh)"] ENV[".env.example
(real .env is gitignored)"] ROOT --> CLAUDE ROOT --> GH ROOT --> HOOKS ROOT --> AWS_TF ROOT --> AZ_TF ROOT --> DOCS ROOT --> TEAR ROOT --> ENV DOCS --> REFARCH AWS_TF -. "outputs (PSK, tunnel IP, VGW ASN)" .-> AZ_TF AZ_TF -. "outputs (Azure GW PIP)" .-> AWS_TF classDef code fill:#10243a,stroke:#3b82f6,color:#e6ecff; classDef cl fill:#2a1d3f,stroke:#c084fc,color:#e6ecff; classDef ops fill:#1a3024,stroke:#34d399,color:#e6ecff; class AWS_TF,AZ_TF code; class CLAUDE,REFARCH cl; class HOOKS,TEAR ops;
Claude Code operating layer
.claude/Every Claude turn against this repo loads CLAUDE.md (the 6 hard rules + audience calibration), routes through settings.json (allow / ask / deny tiers), passes through the PreToolUse hook (live branch check), and can dispatch to one of five specialized subagents that run in their own context with their own tool subset.
6 hard rules · tagging · Azure↔AWS analogues · roadmap"] SET["settings.json
allow · ask · deny · env (TF_IN_AUTOMATION=1)"] HOOK["hooks/block-main-write.sh
PreToolUse · matcher: Bash
blocks git commit/push when on main/master"] AGENTS{{"agents/"}} A1["aws-expert"] A2["azure-expert"] A3["code-reviewer"] A4["security-reviewer"] A5["terraform-reviewer"] TOOL["Bash / Edit / Read / Write"] CLOUD[(Cloud APIs · git · gh)] USER --> CLAUDE CLAUDE -. always loaded .- CMD CLAUDE --> SET SET --> HOOK HOOK -- exit 0 --> TOOL HOOK -- exit 2: blocked --> CLAUDE TOOL --> CLOUD CLAUDE -. dispatch .-> AGENTS AGENTS --> A1 & A2 & A3 & A4 & A5 classDef cl fill:#2a1d3f,stroke:#c084fc,color:#e6ecff; classDef gate fill:#3a2010,stroke:#fbbf24,color:#e6ecff; classDef ext fill:#1a2342,stroke:#94a0c8,color:#e6ecff; class CMD,SET,AGENTS,A1,A2,A3,A4,A5 cl; class HOOK gate; class CLOUD ext;
Branch & secret guardrails — defense in depth
policyThree independent layers enforce "never push to main" and "never read .env". Any one of them is enough; together they catch human mistakes, agent mistakes, and tool-routed mistakes. Bypass is a deliberate --no-verify, not an accident.
(git push · git commit · Read .env)"/] subgraph L1["Layer 1 · settings.json deny patterns"] D1["deny: git push origin main*"] D2["deny: git push --force origin main*"] D3["deny: Read(./.env), Read(./**/.env)"] end subgraph L2["Layer 2 · PreToolUse hook (live state)"] H1["block-main-write.sh
reads git branch --show-current
blocks commit/push when on main/master"] end subgraph L3["Layer 3 · git pre-push (server-bound)"] G1[".githooks/pre-push
rejects push to refs/heads/main or master
incl. deletions"] end RESULT_OK([allowed]) RESULT_BLOCK([blocked + actionable error]) ATTEMPT --> L1 --> L2 --> L3 L1 -- denied --> RESULT_BLOCK L2 -- exit 2 --> RESULT_BLOCK L3 -- non-zero --> RESULT_BLOCK L3 -- pass --> RESULT_OK classDef gate fill:#3a2010,stroke:#fbbf24,color:#e6ecff; classDef ok fill:#163328,stroke:#34d399,color:#e6ecff; classDef bad fill:#3a1717,stroke:#ef4444,color:#e6ecff; class L1,L2,L3,D1,D2,D3,H1,G1 gate; class RESULT_OK ok; class RESULT_BLOCK bad;
Daily workflow loop
processEvery change to a cloud resource follows this loop. The shape comes from claude-dev-toolkit (issue → worktree → plan → confidence-gate → review → PR), adapted to Terraform-driven labs (apply replaces deploy, ref-arch refresh replaces docs, daily teardown replaces "leave it running").
(git worktree per session)"] S2["Plan in chat
state intent · cost · teardown"] S3{"Confidence
≥ 8/10?"} S4["Edit Terraform
+ ref-arch doc same PR"] S5["terraform plan
(allow tier)"] S6["terraform apply
(ask tier)"] S7["Review · code + security + terraform agents"] S8["PR → main
(hooks gate the push)"] S9["Merge"] T1["./teardown-all.sh
(end of day)"] S0 --> S1 --> S2 --> S3 S3 -- no --> S2 S3 -- yes --> S4 --> S5 --> S6 --> S7 --> S8 --> S9 --> T1 --> S0 classDef act fill:#10243a,stroke:#3b82f6,color:#e6ecff; classDef gate fill:#3a2010,stroke:#fbbf24,color:#e6ecff; classDef tear fill:#163328,stroke:#34d399,color:#e6ecff; class S1,S2,S4,S5,S6,S7,S8,S9 act; class S3 gate; class T1 tear;
Three-phase apply (cross-cloud)
runbookNo shared backend → no terraform_remote_state → outputs cross the seam by hand. Phase 1 stands up the Azure-side endpoint so AWS knows where to dial. Phase 2 stands up AWS and emits the PSK + tunnel IP. Phase 3 binds the Azure side to the AWS endpoint and brings BGP up.
(worktree A) participant AwS as AWS session
(worktree B) participant AzC as Azure cloud participant AwC as AWS cloud Op->>AzS: phase 1 — apply (create_connection=false) AzS->>AzC: VNet, GatewaySubnet, PIP, VPN GW AzC-->>AzS: output azure_gw_public_ip Op->>AwS: phase 2 — paste azure_gw_public_ip into tfvars Op->>AwS: terraform apply AwS->>AwC: VPC, IGW, VGW, Customer GW (= Azure PIP), VPN Connection AwC-->>AwS: outputs aws_tunnel1_address, aws_tunnel1_psk, aws_vgw_asn Op->>AzS: phase 3 — paste AWS outputs, flip create_connection=true Op->>AzS: terraform apply AzS->>AzC: Local Network Gateway + Connection (PSK) AzC-->>Op: tunnel UP, BGP peer established AwC-->>Op: tunnel 1 up, 1 BGP route accepted Op->>Op: ICMP test 10.10.1.4 ↔ 10.20.1.88
Teardown path
costHard rule: every resource is destroyable on demand. Each lab ships a destroy.sh wrapper; teardown-all.sh at the repo root walks labs/ and runs them in sequence. Running the teardown daily keeps the $100 AWS promo credit lasting ~6 months and Azure spend in cents/day.
has main.tf?"} S{"has *.tfstate or
.terraform/?"} D["terraform destroy -auto-approve"] SKIP["skip"] DONE([all labs destroyed]) ENV --> ROOT --> W W -- no --> SKIP --> DONE W -- yes --> S S -- no --> SKIP S -- yes --> D --> DONE classDef tear fill:#163328,stroke:#34d399,color:#e6ecff; class ROOT,D,DONE tear;
Toolkit → repo mapping
provenanceEvery concept on this page traces back to a piece of claude-dev-toolkit. The wiki page "Deployed with claude-dev-toolkit" walks each row in detail.
(allow / ask / deny tiers)"] T3["hooks/* (PreToolUse pattern)"] T4["hookify-rules/
block-direct-main-dev
block-env-modification
block-hook-bypass"] T5["templates/agents.md
(specialized subagent pattern)"] T6["plugins/pr-review-toolkit
(multi-agent review)"] T7["templates/worktree-workflow.md
(8/10 confidence gate)"] T8["scripts/claude-session.sh
(crash-proof tmux)"] end subgraph R["cross-cloud-labs"] R1[".claude/CLAUDE.md
6 hard rules + analogues"] R2[".claude/settings.json
terraform/aws/az/git tiers"] R3[".claude/hooks/block-main-write.sh"] R4[".githooks/pre-push
+ settings.json deny patterns"] R5[".claude/agents/
aws- · azure- · code- · security- · terraform-reviewer"] R6["routine 6 — pre-merge multi-agent review"] R7["two-worktree split
aws/* vs azure/* path ownership"] R8["per-session tmux
(when applicable)"] end T1 --> R1 T2 --> R2 T3 --> R3 T4 --> R4 T5 --> R5 T6 --> R6 T7 --> R7 T8 --> R8 classDef cl fill:#2a1d3f,stroke:#c084fc,color:#e6ecff; classDef code fill:#10243a,stroke:#3b82f6,color:#e6ecff; class T1,T2,T3,T4,T5,T6,T7,T8 cl; class R1,R2,R3,R4,R5,R6,R7,R8 code;