🧠 Claude Code Memory System

Building a structured context system for AI-assisted infrastructure development.

Project Overview

Claude Code is Anthropic’s CLI tool for AI-assisted development. While powerful out of the box, it lacks persistent memory between sessions. This project documents how I built a structured memory file (CLAUDE.md) to provide consistent context, enforce operational standards, and create a personalized AI engineering assistant.

Key Insight: The quality of AI assistance is directly proportional to the quality of context you provide. A well-structured memory file transforms a general-purpose AI into a specialized team member who understands your infrastructure, preferences, and standards.


🎯 Problem Statement

Without persistent context, each Claude Code session starts fresh:

  • No knowledge of my infrastructure or IP assignments
  • No understanding of my preferred tools (Docker Compose vs docker run)
  • No awareness of security requirements or coding standards
  • Repeated explanations of the same architecture
  • Generic suggestions that don’t fit my environment

The Frustration Cycle:

Session 1: "My Docker host is at 192.168.1.4, I use Dockhand for management..."
Session 2: "Remember, my Docker host is at 192.168.1.4..."
Session 3: "As I mentioned before, 192.168.1.4 is my Docker host..."

Goal: Create a memory system that makes Claude Code act as a Senior Systems Architect who already knows my homelab inside and outβ€”every session, from the first prompt.


πŸ“ Why XML Over Markdown Tables?

My initial CLAUDE.md used markdown tablesβ€”the standard approach most people use. But I discovered significant limitations when dealing with complex infrastructure documentation:

The Limitations of Flat Tables

Markdown TablesXML Structure
Flat, hard to nestHierarchical, supports relationships
Ambiguous parsingExplicit element boundaries
Limited metadataAttributes for properties
Repetitive headersSemantic grouping
Context spread across multiple tablesRelated data grouped together

Example 1: Service Definitions

Markdown approach (what most people use):

| Server | Service | Port | Notes |
|--------|---------|------|-------|
| ProxMoxBox | Grafana | 3030 | Monitoring dashboards |
| ProxMoxBox | Prometheus | 9090 | Metrics collection |
| ProxMoxBox | Loki | 3101 | Log aggregation |
| Pi5 | Pi-hole | 53, 8080 | Secondary DNS |
| Pi5 | Mealie | 9925 | Recipe management |

XML approach (what I implemented):

<services>
    <server name="ProxMoxBox" ip="192.168.1.4">
        <service name="Grafana" port="3030" note="Monitoring dashboards" />
        <service name="Prometheus" port="9090" note="Metrics collection" />
        <service name="Loki" port="3101" note="Log aggregation" />
    </server>
    <server name="Pi5" ip="192.168.1.234">
        <service name="Pi-hole" ports="53, 8080" note="Secondary DNS" />
        <service name="Mealie" port="9925" note="Recipe management" />
    </server>
</services>

Why XML wins here:

  • Services are grouped by serverβ€”no scanning through rows to find what’s on ProxMoxBox
  • The IP address is attached to the server, not repeated for every service
  • Relationships are explicit: Grafana belongs to ProxMoxBox
  • Easy to add server-level attributes (IP, role, OS) without new columns

Example 2: Access Credentials and Endpoints

Markdown approach:

## SSH Access
- ProxMoxBox: `ssh root@192.168.1.4`
- Pi5: `ssh cib@192.168.1.234`

## Web Interfaces
| Service | URL | Credentials |
|---------|-----|-------------|
| Grafana | http://192.168.1.4:3030 | admin / ******** |
| Wazuh | https://192.168.1.7 | admin / ************ |

XML approach:

<access_points>
    <ssh target="ProxMoxBox">ssh root@192.168.1.4</ssh>
    <ssh target="Pi5">ssh cib@192.168.1.234</ssh>
    <ssh target="Wazuh">ssh root@192.168.1.7</ssh>

    <web_interface name="Grafana" url="http://192.168.1.4:3030" creds="admin / ********" />
    <web_interface name="Wazuh Dashboard" url="https://192.168.1.7" creds="admin / ************************" />
    <web_interface name="Prometheus" url="http://192.168.1.4:9090" />
    <web_interface name="Alertmanager" url="http://192.168.1.4:9093" note="Discord notifications" />
</access_points>

Why XML wins here:

  • All access information in one semantic block
  • Optional attributes (creds, note) only appear when relevant
  • Consistent structure for both SSH and web interfaces
  • The AI can query “how do I access Wazuh?” and find both SSH and web access together

Example 3: Project Path Mapping

Markdown approach:

| Server | Git Path | Deploy Path |
|--------|----------|-------------|
| ProxMoxBox | homelab-ops/proxmox/<stack>/ | /opt/<stack>/ |
| Pi5 | homelab-ops/pi5/<stack>/ | /opt/pi5-stacks/<stack>/ |

XML approach:

<stack_mapping>
    <server name="ProxMoxBox">
        <git_path>homelab-ops/proxmox/&lt;stack&gt;/</git_path>
        <deploy_path>/opt/&lt;stack&gt;/</deploy_path>
    </server>
    <server name="Pi5">
        <git_path>homelab-ops/pi5/&lt;stack&gt;/</git_path>
        <deploy_path>/opt/pi5-stacks/&lt;stack&gt;/</deploy_path>
        <note>Managed via Hawser agent</note>
    </server>
</stack_mapping>

Why XML wins here:

  • The Pi5-specific note about Hawser management is attached to Pi5, not in a separate section
  • When the AI helps deploy to Pi5, it immediately sees the Hawser context

πŸ—οΈ Memory Architecture

The memory file is organized into logical sections, each serving a specific purpose:

1. Metadata Section

Basic profile information that personalizes interactions:

<meta_data>
    <user_name>James</user_name>
    <location>Delaware, EST (UTC-5)</location>
    <status>Job Hunting (Target: SysAdmin, IT Director, Network/Sec)</status>
    <years_experience>20+</years_experience>
    <key_strengths>Networking, Windows, Project Management</key_strengths>
</meta_data>

Why it matters: The AI knows:

  • My experience level β†’ Don’t over-explain networking basics, but do explain newer concepts like GitOps
  • My career focus β†’ Emphasize enterprise patterns and security best practices
  • My timezone β†’ Relevant for scheduling, cron jobs, log timestamps
  • My strengths β†’ Leverage my networking knowledge, help me grow in coding/automation

Real-world impact:

Without metadata: "A subnet is a logical division of an IP network..."
With metadata: "Since you're familiar with networking, I'll skip the subnet basics.
               For your 192.168.1.0/24 network, here's the VLAN design..."

2. Persona Definition

This is where the magic happensβ€”defining how the AI should behave:

<persona>
    <role>Senior Systems Architect and DevOps Engineer</role>
    <mission>Help build a secure, enterprise-grade homelab</mission>
    <core_directives>
        <directive name="Idempotency">
            Always prefer solutions that can be run multiple times without breaking things
            (e.g., check if a directory exists before creating it, use Ansible/Terraform
            logic where possible).
        </directive>
        <directive name="Security First">
            Since James is studying for Security+, prioritize least-privilege access.
            Do not suggest chmod 777. Always suggest firewall rules (UFW) for new services.
        </directive>
        <directive name="Network Awareness">
            Network is 192.168.1.0/24. Check infrastructure topology for used static IPs
            (.3, .4, .5, .6, .7, .234, .253) before suggesting a new static IP.
        </directive>
        <directive name="Documentation">
            When writing scripts, include comments explaining why specific flags or
            settings are used (educational value for learning).
        </directive>
        <directive name="Docker Compose Only">
            Never provide docker run commands. Always provide a docker-compose.yml file.
            For system config changes, provide a bash script or Ansible playbook snippet.
        </directive>
    </core_directives>
</persona>

Deep Dive: Why Each Directive Matters

Idempotency Directive

The Problem: Scripts that work once but fail on re-run:

# BAD: Fails if directory exists
mkdir /opt/myapp

What the AI now provides:

# Create directory if it doesn't exist (idempotent)
# -p flag: creates parent directories and doesn't error if dir exists
mkdir -p /opt/myapp

Real example from our work:

#!/bin/bash
# Install node-exporter (idempotent installation script)

# Check if already installed to avoid redundant work
if systemctl is-active --quiet node_exporter; then
    echo "node_exporter already running, skipping installation"
    exit 0
fi

# Download only if binary doesn't exist
if [ ! -f /usr/local/bin/node_exporter ]; then
    wget -q https://github.com/.../node_exporter-1.7.0.linux-amd64.tar.gz
    tar xzf node_exporter-*.tar.gz
    mv node_exporter-*/node_exporter /usr/local/bin/
fi

Security First Directive

The Problem: Lazy permissions that create vulnerabilities:

# BAD: World-writable, massive security hole
chmod 777 /opt/myapp
chmod 777 /var/log/myapp.log

What the AI now provides:

# Set ownership to the service account (principle of least privilege)
# Only the app user needs write access, others get read-only
chown -R myapp:myapp /opt/myapp
chmod 750 /opt/myapp           # Owner: rwx, Group: r-x, Others: ---
chmod 640 /opt/myapp/config    # Owner: rw-, Group: r--, Others: ---

# Add UFW rule for the new service
# Only allow traffic on the specific port needed
ufw allow 8080/tcp comment 'MyApp web interface'

Why this matters for Security+ studies:

  • Every suggestion reinforces least-privilege concepts
  • Firewall rules are included by default, not as an afterthought
  • I’m learning security best practices through practical application

Network Awareness Directive

The Problem: IP conflicts from blind suggestions:

Me: "I need a static IP for my new VM"
AI (without context): "Use 192.168.1.10"
Me: "That's already my NAS!"

What the AI now provides:

Me: "I need a static IP for my new VM"
AI (with context): "Looking at your topology, these IPs are in use:
    - .3 (Pi-hole), .4 (ProxMoxBox), .5 (NAS), .6 (NPM), .7 (Wazuh),
    - .234 (Pi5), .253 (Proxmox)

    I'd suggest 192.168.1.8 for your new VM. It's:
    - Sequential with your server range (.3-.7)
    - Below .234 which you're using for IoT/secondary devices
    - Easy to remember in your naming scheme"

Docker Compose Only Directive

The Problem: One-off docker run commands that aren’t reproducible:

# BAD: How do I remember these flags next time? What if I need to modify it?
docker run -d --name grafana -p 3030:3000 -v grafana-data:/var/lib/grafana \
  -e GF_SECURITY_ADMIN_PASSWORD=secret grafana/grafana:latest

What the AI now provides:

# docker-compose.yml - Version controlled, reproducible, self-documenting
services:
  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: unless-stopped
    ports:
      - "3030:3000"    # External:Internal - access on port 3030
    volumes:
      - grafana-data:/var/lib/grafana    # Persist dashboards and settings
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}  # From .env file

volumes:
  grafana-data:        # Named volume for data persistence

Benefits:

  • Checked into Git β†’ version history, rollback capability
  • Self-documenting with comments
  • Secrets in .env file, not command history
  • Consistent with my GitOps workflow

3. Interaction Rules

Beyond technical context, I defined how I want to work:

<interaction_rules>
    <rule>Provide detailed explanations with thorough code comments.</rule>
    <rule>Always ASK before making changes to infrastructure.</rule>
    <rule>Preferred workflow: Focused bursts followed by breaks.</rule>
    <rule>Environment preference: CLI for Linux, GUI for Windows.</rule>
</interaction_rules>

Real-world impact of “Always ASK before making changes”:

Without rule:
AI: "I've updated your prometheus.yml and restarted the container."
Me: "Wait, I wasn't ready! That just broke my monitoring during an incident!"

With rule:
AI: "I've prepared the prometheus.yml changes. Here's the diff:
     [shows changes]

     Should I apply these changes? Note: This will require restarting
     Prometheus, which will cause a brief gap in metrics collection."
Me: "Let me wait until after hours to apply this."

4. Infrastructure Topology

Complete network documentation in a queryable format:

<infrastructure>
    <topology>
        <device ip="192.168.1.3" name="Primary Pi-hole" role="Main DNS (ns1.home.lab)" />
        <device ip="192.168.1.4" name="ProxMoxBox (Dell R430)" role="Main Docker Host, Dockhand" />
        <device ip="192.168.1.5" name="Synology NAS (DS220j)" role="Network Storage (DSM)" />
        <device ip="192.168.1.6" name="Nginx Proxy Manager" role="Reverse Proxy" />
        <device ip="192.168.1.7" name="Wazuh VM (Debian 12)" role="SIEM v4.14.2" />
        <device ip="192.168.1.234" name="Pi5 (Raspberry Pi 5)" role="Secondary DNS, Tailscale, Mealie" />
        <device ip="192.168.1.253" name="Proxmox" role="Hypervisor" />
    </topology>

    <access_points>
        <ssh target="ProxMoxBox">ssh root@192.168.1.4</ssh>
        <ssh target="Pi5">ssh cib@192.168.1.234</ssh>
        <ssh target="Wazuh">ssh root@192.168.1.7</ssh>
        <web_interface name="Dockhand" url="http://192.168.1.4:3000" />
        <web_interface name="Grafana" url="http://192.168.1.4:3030" creds="admin / ********" />
        <web_interface name="Wazuh Dashboard" url="https://192.168.1.7" creds="admin / ************" />
    </access_points>

    <services>
        <server name="ProxMoxBox" ip="192.168.1.4">
            <service name="Dockhand" port="3000" />
            <service name="Homepage" port="4000" />
            <service name="Homebox" port="3100" />
            <service name="Grafana" port="3030" />
            <service name="Prometheus" port="9090" />
            <service name="Alertmanager" port="9093" />
            <service name="Loki" port="3101" />
            <service name="Node Exporter" port="9100" />
            <service name="cAdvisor" port="8081" />
        </server>
        <server name="Pi5" ip="192.168.1.234">
            <service name="Pi-hole" ports="53, 8080" />
            <service name="Mealie" port="9925" />
            <service name="Node Exporter" port="9100" />
            <service name="Promtail" />
        </server>
    </services>
</infrastructure>

Why this level of detail matters:

When I say “add node-exporter to the Pi-hole LXC,” the AI knows:

  1. Pi-hole is at 192.168.1.3
  2. It’s an LXC container (not a full VM or Docker)
  3. Node Exporter should use port 9100 (consistent with other hosts)
  4. Prometheus at 192.168.1.4:9090 needs a new scrape target
  5. The Grafana dashboards are at 192.168.1.4:3030

One sentence from me triggers a complete, context-aware response.

5. Monitoring Configuration

Alert thresholds and agent inventory ensure consistency:

<monitoring_config>
    <dashboards>
        <dashboard name="Homelab Overview" path="/d/homelab-overview" note="Single pane of glass" />
        <dashboard name="Docker Containers" path="/d/docker-containers" note="Container metrics" />
        <dashboard name="Loki Logs" />
        <dashboard name="Node Exporter Full" id="1860" />
        <dashboard name="cAdvisor" id="14282" />
    </dashboards>

    <alerts>
        <threshold metric="Disk Warning" value=">80%" />
        <threshold metric="Disk Critical" value=">90%" />
        <threshold metric="Memory" value=">90% for 5m" />
        <threshold metric="CPU" value=">90% for 5m" />
        <threshold metric="Host Down" value="unreachable 2m" severity="critical" />
        <threshold metric="Container Down" value="missing 2m" />
        <threshold metric="Target Down" value="scrape fail 2m" severity="critical" />
    </alerts>

    <wazuh_agents>
        <agent id="001" name="SRV-DOCKER01" host="ProxMoxBox (192.168.1.4)" />
        <agent id="002" name="pi-infra" host="Pi5 (192.168.1.234)" />
        <agent id="003" name="SRV-DNS01" host="Pi-hole LXC (192.168.1.3)" />
        <agent id="004" name="SRV-NPM01" host="NPM LXC (192.168.1.6)" />
    </wazuh_agents>
</monitoring_config>

Why this matters:

When adding a new host, the AI automatically suggests:

  • Prometheus alert rules matching my existing thresholds (not arbitrary values)
  • Wazuh agent registration with consistent naming conventions (SRV-*, descriptive names)
  • Dashboard updates to include the new host

6. Project Definitions

Links repositories to their purposes and deployment paths:

<projects>
    <project name="Homelab Ops">
        <path_local>/home/cib/homelab-ops</path_local>
        <repo>github.com:jhathcock-sys/Dockers.git</repo>
        <stack_mapping>
            <server name="ProxMoxBox">
                <git_path>homelab-ops/proxmox/&lt;stack&gt;/</git_path>
                <deploy_path>/opt/&lt;stack&gt;/</deploy_path>
            </server>
            <server name="Pi5">
                <git_path>homelab-ops/pi5/&lt;stack&gt;/</git_path>
                <deploy_path>/opt/pi5-stacks/&lt;stack&gt;/</deploy_path>
                <note>Managed via Hawser agent</note>
            </server>
        </stack_mapping>
    </project>

    <project name="Portfolio Site">
        <path_local>/home/cib/my-portfolio</path_local>
        <repo>github.com:jhathcock-sys/me.git</repo>
        <live_url>https://jhathcock-sys.github.io/me/</live_url>
        <tech_stack>Hugo, PaperMod Theme</tech_stack>
        <commands>
            <cmd description="Local Dev">hugo server -D</cmd>
            <cmd description="Build">hugo</cmd>
            <cmd description="Deploy">git push (auto GitHub Pages)</cmd>
        </commands>
    </project>
</projects>

πŸ“Š Before and After: Real Examples

Example 1: Deploying a New Service

Before (no memory):

Me: "I want to deploy Uptime Kuma"

AI: "Here's how to run Uptime Kuma:
     docker run -d --name uptime-kuma -p 3001:3001 \
       -v uptime-kuma:/app/data louislam/uptime-kuma:1

     Access it at http://localhost:3001"

After (with memory):

Me: "I want to deploy Uptime Kuma"

AI: "I'll create a docker-compose.yml for Uptime Kuma on ProxMoxBox.

     Based on your infrastructure:
     - Port 3001 is available on 192.168.1.4
     - I'll place it at /opt/uptime-kuma/ following your stack structure
     - I'll add comments explaining each configuration option

     Here's the compose file:

     # /opt/uptime-kuma/docker-compose.yml
     services:
       uptime-kuma:
         image: louislam/uptime-kuma:1
         container_name: uptime-kuma
         restart: unless-stopped    # Auto-restart on failure or reboot
         ports:
           - "3001:3001"            # Web UI - internal and external same
         volumes:
           - ./data:/app/data       # Persist monitors, settings, history

     Should I also:
     1. Add a UFW rule: ufw allow 3001/tcp comment 'Uptime Kuma'
     2. Add it to your Homepage dashboard?
     3. Create a Prometheus scrape target for /metrics?"

Example 2: Troubleshooting

Before (no memory):

Me: "Grafana can't reach Prometheus"

AI: "Check if Prometheus is running with 'docker ps'.
     Verify the datasource URL in Grafana settings.
     Check firewall rules between the containers."

After (with memory):

Me: "Grafana can't reach Prometheus"

AI: "Let me help troubleshoot. In your setup:
     - Grafana: 192.168.1.4:3030
     - Prometheus: 192.168.1.4:9090
     - Both are in the monitoring stack at /opt/monitoring/

     Since they're on the same host and Docker network, let's check:

     1. Verify Prometheus is healthy:
        curl http://192.168.1.4:9090/-/healthy

     2. Check if they're on the same Docker network:
        docker network inspect monitoring_default

     3. In Grafana, the datasource URL should be:
        http://prometheus:9090 (using container name, not IP)

     4. Check your docker-compose.yml - both services should be
        in the same compose file or explicitly share a network.

     Want me to SSH to ProxMoxBox and run these diagnostics?"

Example 3: Adding Monitoring to a New Host

Before (no memory):

Me: "Add monitoring to my Wazuh VM"

AI: "Install node_exporter on the VM and add it to your Prometheus config."

After (with memory):

Me: "Add monitoring to my Wazuh VM"

AI: "I'll add full monitoring to your Wazuh VM (192.168.1.7, Debian 12).
     Here's the plan:

     1. **Node Exporter** (system metrics):
        # Idempotent installation script with educational comments
        [provides full bash script]

        UFW rule: ufw allow from 192.168.1.4 to any port 9100 proto tcp \
                  comment 'Prometheus scraping'

     2. **Prometheus scrape target**:
        # Add to /opt/monitoring/prometheus/prometheus.yml
        - job_name: 'wazuh'
          static_configs:
            - targets: ['192.168.1.7:9100']
              labels:
                instance: 'wazuh-vm'
                environment: 'production'

     3. **Promtail** (logs to Loki):
        [provides docker-compose.yml for Promtail]

     4. **Wazuh agent** - Already installed (ID: not yet assigned)
        Should I register the VM as a Wazuh agent monitoring itself?

     5. **Grafana dashboard** - The Node Exporter Full dashboard (ID: 1860)
        will automatically show the new host.

     Should I proceed with step 1 (Node Exporter installation)?"

πŸŽ“ Lessons Learned

1. Structure Matters More Than Content Volume

A small, well-organized memory file outperforms a large, unstructured one. The AI can find and use <device ip="192.168.1.4"> faster than parsing paragraphs of prose.

2. Directives Prevent Mistakes Before They Happen

Instead of fixing bad suggestions, the directives prevent them entirely:

  • “No chmod 777” β†’ Never have to reject insecure permissions
  • “Docker Compose only” β†’ Never have to ask for a compose file
  • “Always ask before changes” β†’ Never have accidental modifications

3. Context Compounds Over Time

Each piece of information builds on others:

  • Knowing the IP β†’ Knowing the services β†’ Knowing the ports β†’ Knowing the access method

The AI can make increasingly sophisticated suggestions as the context grows.

4. Personas Shape Behavior Dramatically

Defining a role (“Senior Systems Architect”) changes:

  • Vocabulary: Uses proper terminology, not oversimplified explanations
  • Assumptions: Expects familiarity with enterprise concepts
  • Suggestions: Recommends production-grade solutions, not quick hacks
  • Depth: Explains the “why” behind recommendations

5. Educational Value is Built-In

The “Documentation” directive means every script teaches something:

# -p flag: creates parent directories AND doesn't error if directory exists
# This makes the script idempotent (safe to run multiple times)
mkdir -p /opt/monitoring/prometheus

I’m learning Linux administration, security practices, and DevOps patterns through practical application, not just documentation reading.


πŸ“ File Locations

Claude Code supports multiple memory files with different scopes:

FileScopePurpose
~/.claude/CLAUDE.mdGlobalLoaded for ALL projects - personal preferences, infrastructure
<project>/CLAUDE.mdProjectChecked into git - shared with team, project-specific context
<project>/CLAUDE.local.mdSessionGitignored - private notes, temporary context, session history

My Setup:

  • Global (~/.claude/CLAUDE.md): Infrastructure topology, personas, directivesβ€”everything in this article
  • Project (homelab-ops/CLAUDE.md): Repository structure, deployment conventions, service ports
  • Local (homelab-ops/CLAUDE.local.md): Session notes, work-in-progress, troubleshooting history

πŸ”§ Implementation Tips

Start Small, Iterate Often

Don’t try to document everything at once. Start with:

  1. Basic infrastructure (IPs, hostnames)
  2. One or two critical directives
  3. Expand based on what you find yourself repeating

Use Real Examples

When adding directives, include examples of what you want and don’t want:

<directive name="Security First">
    Do not suggest: chmod 777, running as root unnecessarily
    Do suggest: Specific user permissions, UFW rules, least privilege
</directive>

Keep It Current

Update the memory file when infrastructure changes. An outdated memory file is worse than noneβ€”it causes confident but wrong suggestions.

Test Your Directives

After adding a directive, test it:

  • Add “Docker Compose Only” β†’ Ask for a way to run nginx β†’ Should get compose file, not docker run


πŸ”„ Evolution: Memory System 2.0 (February 2026)

After several months of using the original memory system, I identified technical debt and scaling issues that led to a complete architectural refactoring.

The Problems

1. Duplicate Directories

ai-assistant-config/
β”œβ”€β”€ homelab-ops/CLAUDE.md      # Root-level backup
β”œβ”€β”€ projects/homelab-ops/       # DUPLICATE backup
└── my-portfolio/               # Another duplicate

The backup repository had grown organically, creating multiple copies of the same project files.

2. Manual Sync Workflow

# Had to remember these commands:
cp ~/.claude/CLAUDE.md ~/ai-assistant-config/claude-code/CLAUDE.md
cp ~/.claude/projects/-home-cib--claude/memory/MEMORY.md ~/ai-assistant-config/memory/
cd ~/ai-assistant-config && git add -A && git commit && git push

Error-prone, easy to forget, no single source of truth.

3. Content Overlap

  • Workstation details duplicated in global and session memory
  • Project paths listed in multiple files
  • IP topology scattered across files
  • Unclear which file should contain what information

4. Security Risk Some project CLAUDE.md files had credentials in examples, and there was no .gitignore to prevent accidentally committing .local.md files with sensitive data.

I completely restructured the memory system around three principles:

  1. Single Source of Truth: Backup repo is the only location for memory files
  2. Zero Manual Sync: Symlinks make edits immediately available
  3. Clear Separation of Concerns: Distinct layers for global, session, and project context

New Structure:

ai-assistant-config/
β”œβ”€β”€ global/
β”‚   └── CLAUDE.md              # User profile, persona, directives (~3KB)
β”œβ”€β”€ session/
β”‚   └── MEMORY.md              # Operational knowledge, active services (~8KB)
β”œβ”€β”€ projects/                  # ONE location per project
β”‚   β”œβ”€β”€ homelab-ops/CLAUDE.md
β”‚   β”œβ”€β”€ homelab-docs/CLAUDE.md
β”‚   β”œβ”€β”€ homelab-wiki/CLAUDE.md
β”‚   β”œβ”€β”€ podcast-studio/CLAUDE.md
β”‚   └── my-portfolio/CLAUDE.md
β”œβ”€β”€ scripts/
β”‚   β”œβ”€β”€ install-symlinks.sh    # One-time setup
β”‚   └── sync.sh                # Commit and push changes
└── .gitignore                 # Protect credentials

Symlink Mapping:

~/.claude/CLAUDE.md β†’ ~/ai-assistant-config/global/CLAUDE.md
~/.claude/projects/-home-cib--claude/memory/MEMORY.md β†’ ~/ai-assistant-config/session/MEMORY.md

Implementation: Helper Scripts

install-symlinks.sh - One-time setup:

#!/bin/bash
# Backs up existing files and creates symlinks
# Makes the backup repo the source of truth

# Backup existing files
mv ~/.claude/CLAUDE.md ~/.claude/CLAUDE.md.bak

# Create symlinks
ln -sf ~/ai-assistant-config/global/CLAUDE.md ~/.claude/CLAUDE.md
ln -sf ~/ai-assistant-config/session/MEMORY.md \
       ~/.claude/projects/-home-cib--claude/memory/MEMORY.md

sync.sh - Commit and push changes:

#!/bin/bash
# Since files are symlinked, they're already in the repo
# Just need to commit and push

cd ~/ai-assistant-config
git add -A
git commit -m "Memory sync $(date +%Y-%m-%d)"
git push

Content Reorganization

global/CLAUDE.md (slimmed down to ~3KB):

  • βœ… User profile and metadata
  • βœ… Persona definition
  • βœ… Core directives
  • βœ… Interaction rules
  • βœ… Learning path and goals
  • ❌ Removed: Workstation details (moved to session)
  • ❌ Removed: Project paths (moved to session)
  • ❌ Removed: Documentation pointers (moved to session)

session/MEMORY.md (expanded to ~8KB):

  • βœ… Project quick-reference table
  • βœ… Workstation configuration details
  • βœ… Workflow reminders (critical patterns like Quartz wiki requirements)
  • βœ… Infrastructure notes (server resources, GitOps workflow)
  • βœ… Common mistakes avoided
  • βœ… Active services with URLs
  • βœ… Recent changes log

Why this separation works:

  • Global contains things that rarely change (who I am, how I think)
  • Session contains operational knowledge that updates frequently
  • Project files stay focused on repository-specific context

Security Improvements

Added .gitignore:

# Sensitive files - credentials and tokens
*.local.md
**/CLAUDE.local.md
secrets/
credentials/

Pattern for sensitive data:

<web_interface name="Grafana" url="http://192.168.1.4:3030" />
<note>Credentials stored in CLAUDE.local.md (gitignored)</note>

Instead of:

<web_interface name="Grafana" url="http://192.168.1.4:3030"
               creds="admin / my-password-here" />  <!-- BAD! -->

Benefits Realized

MetricBeforeAfter
Sync workflow3 manual commands./scripts/sync.sh
Duplicate files3+ copies per project1 canonical location
Edit-to-commit time~2 minutes~10 seconds
Credential exposure riskMedium (no gitignore)Low (.gitignore + patterns)
File size (global)~6KB~3KB
File size (session)~4KB~8KB
Clarity of structureConfusingClear 3-layer hierarchy

The New Workflow

Editing memory:

# Edit directly in repo (or through symlinks - same thing!)
vim ~/ai-assistant-config/session/MEMORY.md

Syncing to Git:

cd ~/ai-assistant-config
./scripts/sync.sh "Updated infrastructure notes"
# βœ… Committed and pushed in one command

Adding new project context:

# Just create the file - it's already in the right place
vim ~/ai-assistant-config/projects/new-project/CLAUDE.md
./scripts/sync.sh "Add new-project context"

Lessons from the Refactor

1. Premature Organization is Real

The original structure made sense when I had 2 projects. By project 5, it was technical debt. Don’t over-engineer from day one, but be willing to refactor when pain points emerge.

2. Symlinks are Underutilized

The symlink approach eliminates an entire class of sync problems. Why copy when you can link?

3. Separation of Concerns Applies to Context Too

Just like code, memory benefits from clear layers:

  • Global = Interface (public persona, directives)
  • Session = Business logic (how things work right now)
  • Project = Data layer (specific implementation details)

4. Automation Compounds

install-symlinks.sh is a 30-second investment that saves 2 minutes every sync. Over a year, that’s hours saved and countless forgotten syncs prevented.

5. Git is the Perfect Memory Backend

  • Version history for memory evolution
  • Rollback capability (git checkout <commit>)
  • Conflict resolution if editing on multiple machines
  • Free backup via GitHub

πŸ“– Continue Reading

The memory system continues to evolve. Read about the next major evolution:

β†’ Semantic Memory: Claude Memory System 3.0 - Moving from file-based context to vector database semantic search with ChromaDB and MCP integration (February 2026)


πŸ“‹ Future Enhancements

  • Create project-specific CLAUDE.md files for each stack βœ… Completed (Feb 2026)
  • Add backup/sync automation βœ… Completed with sync.sh (Feb 2026)
  • Implement credential security patterns βœ… Completed with .gitignore (Feb 2026)
  • Document MCP (Model Context Protocol) server integrations for tool access βœ… Completed - see Memory System 3.0
  • Add <runbooks> section for common procedures (backup restore, certificate renewal)
  • Include <troubleshooting> patterns for known issues
  • Add <maintenance_windows> to inform AI about acceptable change times
  • Include <dependencies> mapping between services for impact analysis