Article Image
read

Claude Code has a /loop command that runs a prompt on a recurring interval. Mine checks Slack every minute, reads threads, checks the codebase, and replies on my behalf while I’m on a family trip to Sydney.

The problem: loops auto-expire after 3 days. And if the session dies, the loop dies with it.

Here’s how I made it run indefinitely.

The 3 pieces

1. Persistent memory for cross-session state.

Claude Code has a memory directory at ~/.claude/projects/<project>/memory/. Files here survive across sessions. I store 3 things: the agent’s full config, a JSON of already-handled message timestamps, and a rolling conversation log with people context.

When a new session starts and I say “resume monitoring”, Claude reads these files and picks up exactly where it left off.

2. tmux for a persistent terminal session.

Claude runs inside a tmux session called slack-monitor. It keeps running in the background even when I’m not looking. I can attach anytime with tmux attach -t slack-monitor and detach with Ctrl+B, D.

3. macOS launchctl to restart the session daily.

This is the key piece. Every day at 1pm, a LaunchAgent kills the old tmux session and starts a fresh one with Claude, waits for it to boot, then sends the resume prompt.

Why not cron?

I tried cron first. Didn’t work.

Cron runs in a minimal environment without access to your OAuth session or keychain. Claude Code sat there saying “Not logged in.” A LaunchAgent runs inside your user login session, so it has full auth access. It also fires when your Mac wakes up if it missed the schedule. Cron just skips it.

The restart script

I’d recommend a separate shell script rather than inlining everything in the plist. Easier to reason about, easier to debug.

Save this to ~/.claude/claude-agent-restart.sh:

#!/bin/zsh

# Kill existing session
/opt/homebrew/bin/tmux kill-session -t claude-agent 2>/dev/null
sleep 1

# Start new tmux session with a shell
/opt/homebrew/bin/tmux new-session -d -s claude-agent -c /path/to/your/project

# Launch claude (unset CLAUDECODE in case this runs from inside another session)
/opt/homebrew/bin/tmux send-keys -t claude-agent \
  'unset CLAUDECODE && /opt/homebrew/bin/claude --model sonnet' Enter

# Wait for Claude to boot + load MCP servers
sleep 20

# Send the resume prompt
/opt/homebrew/bin/tmux send-keys -t claude-agent \
  'Your resume prompt here.' Enter

Then chmod +x ~/.claude/claude-agent-restart.sh.

A few things worth noting. The unset CLAUDECODE prevents the “cannot launch inside another Claude Code session” error if you ever test this from within an existing Claude session. The sleep 20 gives Claude time to boot and connect MCP servers. And we launch claude via send-keys rather than as the tmux shell command, so the session survives even if Claude exits.

The LaunchAgent

Save this to ~/Library/LaunchAgents/com.yourname.claude-agent.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.yourname.claude-agent</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/zsh</string>
        <string>/Users/yourname/.claude/claude-agent-restart.sh</string>
    </array>
    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key>
        <integer>9</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>
    <key>StandardOutPath</key>
    <string>/tmp/claude-agent.log</string>
    <key>StandardErrorPath</key>
    <string>/tmp/claude-agent.log</string>
</dict>
</plist>

Then load it:

launchctl load ~/Library/LaunchAgents/com.yourname.claude-agent.plist

The flow

Here’s what happens on schedule, with zero human intervention:

  1. launchctl fires the restart script
  2. Old tmux session gets killed (gracefully clears the old /loop)
  3. New tmux session starts, claude launches inside it
  4. After 20s, tmux send-keys types the resume prompt
  5. Claude reads persistent memory files, sets up /loop, starts working

The /loop runs for up to 3 days. But launchctl restarts the session every few hours, so the 3-day limit never matters. Fresh session, fresh loop, same memory.

My use case: Slack monitoring

I used this to build an autonomous Slack monitor. It watches my channels, checks the codebase when people report bugs, and replies in threads as me. There’s a DRY RUN mode for testing drafts before going live, voice matching so replies don’t sound like an AI, and security rules to prevent leaking sensitive code. That’s a whole separate topic, but the infrastructure above is what makes it possible.

You could use the same pattern for anything: monitoring deploys, triaging GitHub issues, watching logs, polling an API. Any task that benefits from an AI agent running continuously with access to your codebase and tools.

The loop expires. The agent doesn’t have to.


Image

@samwize

¯\_(ツ)_/¯

Back to Home