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

FileLocationLoaded byPurpose
Global user CLAUDE.mdC:\Users\konar\.claude\CLAUDE.md (Windows dev)Claude Code on startupDev 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 repoPer-repo architecture, commands, stack details
VPS global CLAUDE.md/home/claude-runner/.claude/CLAUDE.md on each VPSClaude Code on startupVPS-specific overrides: use Bash (not PowerShell), SOPS lookup order, worktree workflow
VPS global sourcescripts/vps-global-claude.md in this repoDeployed by _setup-claude-env-ansible.shSource 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

EnvironmentShellCLAUDE.md sourceRole
Windows dev workstationPowerShellC:\Users\konar\.claude\CLAUDE.md + projectOrchestrator
vps-i1 / AI-Dev-IO1Bashscripts/vps-global-claude.md + projectmonitoring-ops
bms-4 / AI-Dev-BMS4-1Bashscripts/vps-global-claude.md + projectsaas-dev
vps-h1N/ANo Claude agents run on vps-h1WAHA gateway only

Role environment variables (set by _setup-claude-env-ansible.sh)

P24_CLAUDE_ROLE is set per-machine in ~/.bashrc:

Hostname patternP24_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.md applies 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 onlyC:\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 followscripts/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.md is injected as the worker agent’s system prompt and contains its own ## Credential Safe-Handling Rules section (worker output is posted to GitHub comments, n8n logs, and agent_tasks.output, so the worker needs these rules inline — it does not reliably load the global ~/.claude/CLAUDE.md security section, see issue #1503). When you change the credential safe-handling rules in scripts/vps-global-claude.md, also update the ## Credential Safe-Handling Rules section in infra/agent-prompts/worker-issue.md so 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 only

What it does:

  • Extracts the security section (everything above the # p24-infra — VPS Agent Global Rules header) from scripts/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.sh deploys), 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.mdVPS Access):

WorkerSSH targetPrivileged write
vps-i1claude-admin@217.154.82.162sudo 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-4root@54.36.123.110plain 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 prompt infra/agent-prompts/worker-issue.md carries its own ## Credential Safe-Handling Rules section 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"   # → 2

4. 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 yet

Confirm 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.yml

Expected 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:

  1. Check if GitHub is reachable: curl -s https://api.github.com/status | python3 -c "import json,sys; print(json.load(sys.stdin)['status']['description'])"
  2. Check SSH key for GitHub: ssh -T git@github.com (as claude-runner)
  3. If the repo is corrupted: git -C /opt/p24-infra status — look for conflict markers or detached HEAD
  4. 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-version on the VPS is more than a few days behind origin/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

  1. 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'"
  2. Force-resync (see Section 5).

  3. Restart any stalled GitHub Actions jobs — they will pick up the fresh env on next session.

  4. 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 from origin/main, revert with git revert -m 1 <merge-sha>, and open a PR targeting main.

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-infra cloned from main (done by Ansible provision-new-vps.yml)
  • The claude-runner user 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.


FilePurpose
scripts/vps-global-claude.mdSource for VPS ~/.claude/CLAUDE.md (incl. credential safe-handling rules)
scripts/sync-worker-claude-md.shOut-of-band immediate push of the credential safe-handling rules to vps-i1 + bms-4 (idempotent, sentinel-delimited)
scripts/check-env-sync.shHash-comparison + pull + setup trigger
scripts/setup-claude-env.shSkill install + hook merge + CLAUDE.md deploy (deprecated wrapper)
ansible/roles/claude-env-sync/tasks/main.ymlAnsible tasks: cron, age key, first-run
ansible/roles/claude-env-sync/templates/setup-claude-env.sh.j2Ansible-managed version of setup script
ansible/roles/claude-env-sync/defaults/main.ymlRole defaults: age key path, claude-runner username
ansible/inventory/hosts.ymlAll VPS hosts: vps-i1, vps-h1, bms-4
ansible/playbooks/vps-i1.ymlvps-i1 playbook (includes claude-env-sync role)
docs/ai-agent-operations.mdFleet overview, auth, per-agent SSH and status
docs/claude-agent-setup.mdInitial Claude Code installation guide (IONOS)