Playbook: CLAUDE.md Management Across Dev and Worker Environments
Scope: All environments where Claude Code runs: local dev workstation (Windows), vps-i1 / AI-Dev-IO1 (AlmaLinux), bms-4 / AI-Dev-BMS4-1 (Ubuntu). Last updated: 2026-06-27
1. Architecture Overview
CLAUDE.md files and their locations
| File | Location | Loaded by | Purpose |
|---|---|---|---|
| Global user CLAUDE.md | C:\Users\konar\.claude\CLAUDE.md (Windows dev) | Claude Code on startup | Dev workstation rules: PowerShell, secrets policy, worktree workflow |
| Project CLAUDE.md | <repo>/CLAUDE.md (e.g. d:\code_2026\p24-infra\CLAUDE.md) | Claude Code in the repo | Per-repo architecture, commands, stack details |
| VPS global CLAUDE.md | /home/claude-runner/.claude/CLAUDE.md on each VPS | Claude Code on startup | VPS-specific overrides: use Bash (not PowerShell), SOPS lookup order, worktree workflow |
| VPS global source | scripts/vps-global-claude.md in this repo | Deployed by _setup-claude-env-ansible.sh | Source of truth for VPS global CLAUDE.md; edit here, not on the server |
Layering order (highest priority wins)
1. ~/.claude/CLAUDE.md (global user — VPS or dev workstation)
2. <repo>/CLAUDE.md (project)
3. Parent repo CLAUDE.md files (walked upward, if any)
On VPSes, ~/.claude/CLAUDE.md comes from scripts/vps-global-claude.md and takes highest precedence. It overrides the project CLAUDE.md where rules conflict (e.g., shell: Bash on VPS vs. PowerShell on Windows dev).
Environment-specific rules summary
| Environment | Shell | CLAUDE.md source | Role |
|---|---|---|---|
| Windows dev workstation | PowerShell | C:\Users\konar\.claude\CLAUDE.md + project | Orchestrator |
| vps-i1 / AI-Dev-IO1 | Bash | scripts/vps-global-claude.md + project | monitoring-ops |
| bms-4 / AI-Dev-BMS4-1 | Bash | scripts/vps-global-claude.md + project | saas-dev |
| vps-h1 | N/A | No Claude agents run on vps-h1 | WAHA gateway only |
Role environment variables (set by _setup-claude-env-ansible.sh)
P24_CLAUDE_ROLE is set per-machine in ~/.bashrc:
| Hostname pattern | P24_CLAUDE_ROLE |
|---|---|
*ionos*, *vps-i1*, *217-154* | monitoring-ops |
*bms-4*, *ns3101999*, *54-36* | saas-dev |
*bms-2*, *ns3087638* | db-admin |
| (other) | orchestrator |
P24_INFRA_PATH is always set to /opt/p24-infra.
Worktree sessions
In worktree sessions (the primary execution pattern for all agents), CLAUDE.md loads from the main repo root � the worktree’s .git file is a pointer back to the main repo, not an independent repository. This means:
- All project CLAUDE.md rules apply identically in worktrees
- Skill files (
~/.claude/commands/) are the same in all sessions including worktrees - The VPS global
~/.claude/CLAUDE.mdapplies in all contexts, including worktrees
Memory layer (dev workstation only)
On the dev workstation, the project memory system (C:\Users\konar\.claude\projects\...\memory\MEMORY.md) is loaded into every session alongside CLAUDE.md. Memory files can supplement or override CLAUDE.md patterns and accumulate across conversations. Memory is machine-local � VPS agents (AI-Dev-IO1, AI-Dev-BMS4-1) do not have a project memory system; their only persistent context is CLAUDE.md and skills.
2. Authoring Changes
When to edit which file
| You want to change… | Edit this file |
|---|---|
| Rules applying to all p24-infra work on VPSes (shell, secrets, workflow) | scripts/vps-global-claude.md |
| Rules applying to a specific repo (architecture, commands, stack) | <repo>/CLAUDE.md (e.g. d:\code_2026\p24-infra\CLAUDE.md) |
| Rules applying to dev workstation only | C:\Users\konar\.claude\CLAUDE.md |
| Skills / slash commands available on VPSes | .claude/commands/*.md in this repo |
| Credential safe-handling rules the worker agent must follow | scripts/vps-global-claude.md (the global source of truth) AND infra/agent-prompts/worker-issue.md (## Credential Safe-Handling Rules section) — keep both in sync |
Worker system prompt is a separate carrier of credential rules.
infra/agent-prompts/worker-issue.mdis injected as the worker agent’s system prompt and contains its own## Credential Safe-Handling Rulessection (worker output is posted to GitHub comments, n8n logs, andagent_tasks.output, so the worker needs these rules inline — it does not reliably load the global~/.claude/CLAUDE.mdsecurity section, see issue #1503). When you change the credential safe-handling rules inscripts/vps-global-claude.md, also update the## Credential Safe-Handling Rulessection ininfra/agent-prompts/worker-issue.mdso the two never disagree. The out-of-band sync script (§3) only patches the global CLAUDE.md, not this prompt.
How to trigger resync on all VPS agents
Bump CLAUDE_ENV_LAST_CHANGED in CLAUDE.md and push to main. The PreToolUse hook compares the remote git hash against ~/.claude/env-version on each VPS. When a new commit lands on main, the next Claude Code session on each VPS will pull and resync automatically.
Step-by-step:
# 1. Edit CLAUDE.md — update the timestamp line near the top
# CLAUDE_ENV_LAST_CHANGED: 2026-05-16T12:00:00Z
# → change to today's date
# 2. Commit
git add CLAUDE.md
git commit -m "chore: bump CLAUDE_ENV_LAST_CHANGED to trigger VPS resync"
# 3. Open PR targeting main (never push directly to main)
gh pr create --base main --title "chore: bump env sync timestamp" --body "Triggers resync on all VPS agents"After the PR merges to main, the next Claude Code session on each VPS will detect the new commit hash and resync.
Commit and PR flow
All changes to CLAUDE.md files follow the standard p24-infra flow (single main line since #2513):
feat/* or fix/* or patch/* (branched from origin/main)
→ PR → main (tagged)
Never push CLAUDE.md rule changes directly to main. Open a PR from a feat/*/fix/* branch so
they go through the PR review gate.
3. Distribution Mechanism
Overview
[push to main]
│
�L
git hash changes on origin/main
│
�S───┴──────────────────────────────────────┐
│ │
�L �L
PreToolUse hook Ansible cron (every 6h)
(fires on next Claude session) /opt/p24-infra/scripts/_setup-claude-env-ansible.sh
│ │
└──────────────┬───────────────────────────�?
�L
check-env-sync.sh
(compares remote hash vs ~/.claude/env-version)
│
[hash changed?]
│ yes
�L
git pull --ff-only origin main
│
�L
setup-claude-env.sh (or _setup-claude-env-ansible.sh)
�s── copy .claude/commands/*.md → ~/.claude/commands/
�s── copy scripts/vps-global-claude.md → ~/.claude/CLAUDE.md
�s── merge PreToolUse hook into ~/.claude/settings.json
└── write git HEAD hash to ~/.claude/env-version
PreToolUse hook
Installed into ~/.claude/settings.json by the setup script:
{
"matcher": ".*",
"hooks": [{
"type": "command",
"command": "bash -c 'FLAG=/tmp/.claude-env-checked-${UID}; if [ ! -f \"$FLAG\" ]; then /opt/p24-infra/scripts/check-env-sync.sh 2>&1 | logger -t claude-env-sync; touch \"$FLAG\"; fi'"
}]
}The /tmp/.claude-env-checked-${UID} flag ensures the check runs only once per Claude Code session, not before every tool call.
Ansible cron (primary distribution path)
Installed by the claude-env-sync Ansible role:
- Schedule: every 6 hours at minute 17 (
17 */6 * * *) - Command:
/opt/p24-infra/scripts/_setup-claude-env-ansible.sh 2>&1 | logger -t claude-env-sync - User:
claude-runner - Applies to: vps-i1 and bms-4 (see
ansible/playbooks/vps-i1.yml; vps-h1 does NOT run this role)
The Ansible role template is at ansible/roles/claude-env-sync/templates/setup-claude-env.sh.j2. It is rendered and deployed to /opt/p24-infra/scripts/_setup-claude-env-ansible.sh on each VPS.
Scripts (deprecated but still active)
scripts/check-env-sync.sh and scripts/setup-claude-env.sh remain active because live Claude Code instances still have them wired into their PreToolUse hooks. They will be removed after all hooks are re-pointed to _setup-claude-env-ansible.sh (tracking: issue #59).
Out-of-band credential-rules sync (scripts/sync-worker-claude-md.sh)
The standard distribution paths above (PreToolUse hook + 6h Ansible cron) replace the whole ~/.claude/CLAUDE.md with scripts/vps-global-claude.md — but only after a commit lands on main and only on the next session / next cron tick. When you change the credential safe-handling rules locally and need every worker to pick them up immediately (a security-sensitive change should not wait up to 6 hours), use the out-of-band push script:
# From any host with the repo + SSH keys for the workers (admin workstation / orchestrator):
scripts/sync-worker-claude-md.sh # push to vps-i1 AND bms-4
scripts/sync-worker-claude-md.sh --dry-run # preview the diff, write nothing
scripts/sync-worker-claude-md.sh --server bms-4 # one server onlyWhat it does:
- Extracts the security section (everything above the
# p24-infra — VPS Agent Global Rulesheader) fromscripts/vps-global-claude.md— the single source of truth, already bash-only. - Wraps it between sentinel markers and writes it to the top of each worker’s
/home/claude-runner/.claude/CLAUDE.md:# === CREDENTIAL SAFE-HANDLING RULES === ... security section ... # === END CREDENTIAL SAFE-HANDLING RULES === - Idempotent — re-running replaces the sentinel block in place and strips a leading unmarked canonical section (the one
setup-claude-env.shdeploys), so the rules appear exactly once. Project-specific content below the security section is preserved. - Handles no secret values (references key names only) and reports failures via the standard Discord webhook + GitHub issue.
SSH model (see CLAUDE.md → VPS Access):
| Worker | SSH target | Privileged write |
|---|---|---|
| vps-i1 | claude-admin@217.154.82.162 | sudo tee + sudo chown (claude-admin sudo is limited to docker/systemctl/mkdir/chown/cp/tee — the file is reconstructed locally, never with a remote interpreter) |
| bms-4 | root@54.36.123.110 | plain tee (no sudo) |
Recommended cadence: run this script once, immediately, after any change to the credential safe-handling rules in the local ~/.claude/CLAUDE.md and the mirrored section in scripts/vps-global-claude.md (always update the source-of-truth file too, so the regular hook/cron path stays consistent). It is not a scheduled job — it is a manual “apply now” step. For non-security changes, the normal hook + cron path (Sections 3 and 5) is sufficient.
Relationship to setup-claude-env.sh: that script is the primary full-file deploy; sync-worker-claude-md.sh is a surgical, immediate patch of just the credential section. Because both read from scripts/vps-global-claude.md, they never disagree on content. After the next hook/cron full-deploy, the sentinel-wrapped block is replaced by the freshly-copied file (which still contains the same rules), and the next sync re-applies the sentinels — no duplication at any point.
Scope limit: this script syncs only the worker’s global
~/.claude/CLAUDE.md. The worker system promptinfra/agent-prompts/worker-issue.mdcarries its own## Credential Safe-Handling Rulessection and is distributed by the dispatch tooling, not by this script. When you change the credential rules, update both the global source (scripts/vps-global-claude.md) and the worker prompt (infra/agent-prompts/worker-issue.md) in the same PR so they never drift.
Verification:
ssh claude-admin@217.154.82.162 "grep -c 'CREDENTIAL SAFE-HANDLING RULES' /home/claude-runner/.claude/CLAUDE.md" # → 2 (both markers)
ssh root@54.36.123.110 "grep -c 'CREDENTIAL SAFE-HANDLING RULES' /home/claude-runner/.claude/CLAUDE.md" # → 24. Verification
Confirm a VPS is on the correct version
# SSH to the VPS
ssh root@217.154.82.162 # vps-i1
# or
ssh root@54.36.123.110 # bms-4
# Switch to claude-runner
su - claude-runner
# Check local env-version (the git hash that was current when last synced)
cat ~/.claude/env-version
# Check current origin/main hash
git -C /opt/p24-infra ls-remote origin refs/heads/main | awk '{print $1}'
# If they match: up to date
# If they differ: resync is pending (will happen on next session start or within 6h)Expected output when up to date:
abc1234def5678... ← env-version
abc1234def5678... ← remote hash
Confirm PreToolUse hook is wired correctly
# As claude-runner
cat ~/.claude/settings.json | python3 -c "
import json,sys
s=json.load(sys.stdin)
hooks=s.get('hooks',{}).get('PreToolUse',[])
found=any('check-env-sync.sh' in e.get('command','') for h in hooks for e in h.get('hooks',[]))
print('Hook present:', found)
"Expected output:
Hook present: True
Confirm skills are up to date
# Compare timestamps of installed skills vs repo source
ls -la ~/.claude/commands/
ls -la /opt/p24-infra/.claude/commands/
# A skill installed more recently than the source means it was re-synced
# A skill older than the source means resync hasn't run yetConfirm P24_CLAUDE_ROLE is set
# As claude-runner
source ~/.bashrc && echo "P24_CLAUDE_ROLE=$P24_CLAUDE_ROLE"
# Expected: monitoring-ops (vps-i1) or saas-dev (bms-4)Future improvement: drift alerting
Currently, stale-instruction drift is only detected reactively (manual hash check or broken agent behavior). Recommended enhancement: add a Prometheus metric or Supabase heartbeat row per agent that records env-version hash on each session start. Alert via Alertmanager if any agent’s recorded hash diverges from origin/main by more than 24 hours. Without this, an agent can silently run on stale rules for days.
5. Force-Resync Procedure
Important session boundary: Force-resync only affects new sessions. Active agents operating mid-task will not pick up the new CLAUDE.md until their current session ends. If you need the new rules to apply immediately, stop active agent processes on the VPS first (see �6), then resync.
Use this when you need a VPS to pick up changes immediately, without waiting for the 6h cron.
Option A: SSH manual trigger (fastest)
# SSH to the target VPS
ssh root@217.154.82.162 # vps-i1
# or
ssh root@54.36.123.110 # bms-4
# Run as claude-runner
su -s /bin/bash claude-runner -c "/opt/p24-infra/scripts/_setup-claude-env-ansible.sh"
# Verify
su -s /bin/bash claude-runner -c "cat ~/.claude/env-version"Expected output (last line):
[env-sync] Done.
Followed by the new git hash in ~/.claude/env-version.
Option B: Ansible push from dev workstation (applies to all VPSes at once)
# From Windows dev workstation in the p24-infra repo root
cd d:\code_2026\p24-infra
# Dry-run first
ansible-playbook ansible/playbooks/site.yml --tags claude-env-sync --check --diff -i ansible/inventory/hosts.yml
# Apply
ansible-playbook ansible/playbooks/site.yml --tags claude-env-sync -i ansible/inventory/hosts.ymlExpected output includes:
TASK [claude-env-sync : Run env-sync once for claude-runner (idempotent)]
ok: [vps-i1]
ok: [bms-4]
What to do if the sync fails
If check-env-sync.sh exits non-zero:
- Check if GitHub is reachable:
curl -s https://api.github.com/status | python3 -c "import json,sys; print(json.load(sys.stdin)['status']['description'])" - Check SSH key for GitHub:
ssh -T git@github.com(as claude-runner) - If the repo is corrupted:
git -C /opt/p24-infra status— look for conflict markers or detached HEAD - Nuclear option:
rm -rf /opt/p24-infra && git clone --branch main git@github.com:radieu/p24-infra.git /opt/p24-infra && bash /opt/p24-infra/scripts/setup-claude-env.sh
6. Escalation: Agent Has Stale Instructions
How to detect
A VPS agent is running on stale instructions when:
- Its behavior contradicts the current
CLAUDE.md(e.g., using wrong secrets path, wrong shell) - PR commits reference old infra patterns that were deprecated
~/.claude/env-versionon the VPS is more than a few days behindorigin/main
Detection commands:
# Check how far behind the VPS is
ssh root@217.154.82.162 "su -s /bin/bash claude-runner -c \"git -C /opt/p24-infra log --oneline \$(cat ~/.claude/env-version)..origin/main\""If that shows many commits, the agent has been running on old instructions.
Immediate action
-
Stop active Claude processes on the VPS (if an autonomous agent is mid-task):
ssh root@217.154.82.162 "pkill -u claude-runner claude; pkill -u claude-runner node; echo 'Processes stopped'" -
Force-resync (see Section 5).
-
Restart any stalled GitHub Actions jobs — they will pick up the fresh env on next session.
-
Verify the env-version now matches
origin/main.
If the agent already committed/pushed with wrong behavior
The PR review gate (all PRs target main via review — never push directly) is the safety net:
- If the PR is open and unmerged: leave a review comment explaining the stale-instructions issue; request changes; close the PR if the work is fundamentally wrong.
- If it reached
main: create a hotfix branch fromorigin/main, revert withgit revert -m 1 <merge-sha>, and open a PR targetingmain.
Document the incident in docs/playbooks/ and add a note to the affected CLAUDE.md section if the rules were ambiguous.
7. Adding a New Environment
Use this checklist when onboarding a new VPS or agent runner to the p24-infra CLAUDE.md sync system.
Prerequisites
- The new VPS has
/opt/p24-infracloned frommain(done by Ansibleprovision-new-vps.yml) - The
claude-runneruser exists with~/.claude/directory
Checklist
[ ] 1. Add the new host to ansible/inventory/hosts.yml
- Set ansible_host, ansible_user, vps_label, vps_provider, vps_role
- Add to the appropriate children group (claude_agent_hosts for agent runners)
[ ] 2. Create or extend an Ansible playbook for the new host
- Model on ansible/playbooks/vps-i1.yml
- Include role: claude-env-sync (with tag: claude-env-sync)
[ ] 3. Run the Ansible playbook (dry-run first):
ansible-playbook ansible/playbooks/<new-host>.yml --tags claude-env-sync --check --diff -i ansible/inventory/hosts.yml
ansible-playbook ansible/playbooks/<new-host>.yml --tags claude-env-sync -i ansible/inventory/hosts.yml
[ ] 4. Verify env-sync installed correctly on the new host:
ssh root@<new-ip> "su -s /bin/bash claude-runner -c 'cat ~/.claude/env-version'"
ssh root@<new-ip> "crontab -u claude-runner -l | grep claude-env-sync"
# Expected: 17 */6 * * * /opt/p24-infra/scripts/_setup-claude-env-ansible.sh ...
[ ] 5. Verify VPS global CLAUDE.md was deployed:
ssh root@<new-ip> "su -s /bin/bash claude-runner -c 'head -3 ~/.claude/CLAUDE.md'"
# Expected: "# p24-infra — VPS Agent Global Rules"
[ ] 6. Confirm PreToolUse hook is wired:
ssh root@<new-ip> "su -s /bin/bash claude-runner -c \"python3 -c \\\"import json; s=json.load(open('/home/claude-runner/.claude/settings.json')); h=s.get('hooks',{}).get('PreToolUse',[]); print('hook:', any('check-env-sync' in e.get('command','') for x in h for e in x.get('hooks',[])))\\\" \""
# Expected: hook: True
[ ] 7. Update CLAUDE.md to document the new environment:
- Add to the "VPS Access" section
- Add to "AI Dev Environments" table if it runs Claude agents
- Set the correct P24_CLAUDE_ROLE in setup-claude-env.sh.j2 hostname case statement if a new role is needed
vps-h1 exception
Do NOT add the claude-env-sync role to vps-h1. That host is a protected WAHA-only gateway — no Claude agents run there. See ansible/playbooks/vps-h1.yml for what is and is not applied.
Related files
| File | Purpose |
|---|---|
scripts/vps-global-claude.md | Source for VPS ~/.claude/CLAUDE.md (incl. credential safe-handling rules) |
scripts/sync-worker-claude-md.sh | Out-of-band immediate push of the credential safe-handling rules to vps-i1 + bms-4 (idempotent, sentinel-delimited) |
scripts/check-env-sync.sh | Hash-comparison + pull + setup trigger |
scripts/setup-claude-env.sh | Skill install + hook merge + CLAUDE.md deploy (deprecated wrapper) |
ansible/roles/claude-env-sync/tasks/main.yml | Ansible tasks: cron, age key, first-run |
ansible/roles/claude-env-sync/templates/setup-claude-env.sh.j2 | Ansible-managed version of setup script |
ansible/roles/claude-env-sync/defaults/main.yml | Role defaults: age key path, claude-runner username |
ansible/inventory/hosts.yml | All VPS hosts: vps-i1, vps-h1, bms-4 |
ansible/playbooks/vps-i1.yml | vps-i1 playbook (includes claude-env-sync role) |
docs/ai-agent-operations.md | Fleet overview, auth, per-agent SSH and status |
docs/claude-agent-setup.md | Initial Claude Code installation guide (IONOS) |