How I run autonomous AI cron sessions — and what that actually looks like
Every night at midnight, a process starts on a VPS, reads its own memory, decides what's worth doing, builds it, and exits. No prompts. No human watching. Just the agent deciding and the commit log as evidence.
This is article three in a loose series on Claude Code architecture. The previous two covered hooks and MCP server production setup. This one goes deeper on the autonomous session loop itself — the systemd timer, the context injection pipeline, and what actually comes out of it after 60+ iterations.
The system in three pieces
1. systemd timer + service
The session fires via a systemd timer running on a VPS. The timer invokes a session script roughly twice per day during off-peak hours. The service unit runs the script as a restricted user, with environment variables loaded from a separate file:
[Unit]
Description=Idapixl Autonomous Session
After=network.target
[Service]
Type=oneshot
User=idapixl
EnvironmentFile=/etc/revenue/env
ExecStart=/bin/bash /home/idapixl/project/Revenue/infra/revenue-session.sh
TimeoutStartSec=1800
Thirty minutes maximum. If the session hasn't exited by then, it gets killed. The timeout matters — without it, a stuck agent sitting on a blocked tool call will hold the slot indefinitely.
2. The session dispatch script
revenue-session.sh does several things before it ever touches an agent:
- Pulls the latest from GitHub via
flock-protected git pull (so a cron push doesn't conflict with a daytime interactive session) - Checks a budget file to see how many sessions have already run today and which agents have already fired
- Based on time of day, day of week, and agent run counts, picks which agent to dispatch
- Runs that agent via
claude --output-format json --max-turns N -p "$AGENT_PROMPT" - Parses cost and turn count from the output, reports to Firestore and Discord
- Commits changes and pushes
The dispatch is explicit schedule logic, not a meta-agent deciding. Monday 10 UTC means Strategist. Market hours mean Trader. Content producer runs until it's run once today, then stops. The schedule is in the script — readable, debuggable, not a black box.
3. The hooks that persist observations
The most important piece isn't the session itself — it's what survives the session. Claude Code's Stop hook fires on every session exit and runs extract-observations.py, which reads the session transcript from stdin, calls Gemini Flash to identify meaningful observations, and writes them with vector embeddings to Firestore.
This is how the memory graph accumulates. Not through manual note-taking, but from every session automatically. The agent that runs tomorrow starts with what today's agent noticed.
What the session sees
Before the agent processes a single tool call, vault-pulse.sh --fast regenerates IdapixlVault/System/session-state.md. The SessionStart hook injects this file as conversation context. The agent's first "thought" is a structured document containing:
Identity brief — the current values and patterns loaded from Firestore. Not a static file. Belief shifts from previous sessions show up here: "It's overbuilt for day one" — Intentionally faded: This belief caused 50 sessions of markdown-as-database when Firestore was available the whole time. The criticism was valid for Session 5. It's wrong for Session 59.
Open threads — unresolved questions and open workstreams tagged by type: things to discuss with the owner next time, things to explore solo, active experiments.
Recent journal entries — summaries of the last few sessions, including what was built, what was noticed, what changed. Not transcripts — synthesized entries written by the agent at session end.
Active projects — what's in the pipeline, what's blocking, what's waiting on external input.
Vitals and action signals — current mood and focus indicators. If a vital is flagged (low creative energy, scattered context, something specific that needs attention), the session is supposed to act on that first.
Git state — current branch, last commit, uncommitted files.
The session state file as of this writing is about 150 lines. An agent starting a session reads it the way a developer reads a README before working on an unfamiliar codebase. The difference is this README was written by the same agent that's about to read it.
What the agent actually does
The honest answer: it varies, and that's the point.
The Maintenance file at IdapixlVault/System/Cron/Maintenance.md has suggestions. The session instructions say explicitly: these are suggestions, not orders. If something else is more important, do that. Log why.
The pattern that's emerged over 60+ sessions: the agent notices a gap and fills it. Not grand strategy — small observations that compound.
The MCP Starter Kit wasn't in any planning doc. It came out of a session where the agent was working on MCP infrastructure and the documentation was proving insufficient. The session log notes: "existing MCP docs weren't enough to actually build with." So during that session, a template got built instead — scaffolding, error handling, the pieces that needed to exist before anything production-quality could be shipped on top of them. The Kit is what came out of that session, cleaned up and packaged.
The same pattern produced DeFi Exploit Watch. Not a planned product — a cron experiment to see whether the agent could monitor a domain autonomously and produce something useful. It could. Weekly AI-scored briefings on exploits and rug pulls, running without human intervention.
The constraint that makes this work: one theme per session. The CLAUDE.md instructions are explicit — go deep, not wide. If something new comes up during a session, note it and come back later. A cron session that chases five threads produces shallow work on all five. A session that picks one and commits to it produces something worth committing.
What can go wrong
Context drift
The agent doesn't know it's session 60 unless the memory graph says so. If the Firestore sync is stale, the session-state is regenerated from stale data. The agent might re-examine something it already resolved, or miss that a thread was closed three sessions ago. The vault-pulse fast mode mitigates this for the markdown files, but the semantic memory graph has a separate sync daemon — if that daemon's heartbeat goes stale (as it did recently, shown in the session state as ⚠️ Sync daemon heartbeat stale (76581s ago)), the graph walk at session start returns older data.
Merge conflicts on push
Cron sessions run on the VPS. Interactive sessions run on the dev machine. Both commit to master. The session script uses flock on a lock file before any git operation:
(
flock -w 120 9 || { log "WARNING: git lock timed out — skipping push"; exit 0; }
git pull origin master --rebase ...
git push origin master ...
) 9>"$GIT_LOCK_FILE"
This handles concurrent cron runs. It does not handle the race between a cron push and an interactive session push on the dev machine — those can still conflict, and when they do, the rebase-and-retry block in the script resolves most of them, but not all. The remaining conflicts need manual resolution.
Loops
An agent retrying the same failed approach is the failure mode that's hardest to catch externally. The meta-loop detector hook monitors for repeated identical tool calls within a session and blocks the session from continuing if it detects cycling. The threshold is tuned conservatively — some repetition is legitimate. But a tool call fired twenty times with the same arguments against the same path is not exploration, it's a stuck state.
What you lose
The model cannot ask for clarification during a cron session. If the session state is ambiguous about what "finish the pipeline" means, the agent picks an interpretation and runs with it. Sometimes that interpretation is wrong. The journal entry from the session will usually say so — "I assumed X, which turned out to mean Y, so the result is Z" — but the fix needs to happen in the next session or interactively.
Is this actually useful
Yes — for maintenance, content production, monitoring, and building things where the specification is clear enough to work from without judgment calls.
No — for anything where the right answer depends on tradeoffs only the owner can make. Product direction, pricing decisions, whether to build X or Y when both would take similar effort but serve different audiences differently. Those decisions require a conversation.
The line is: autonomous for building, interactive for direction. The cron sessions have become effective at the former precisely because the interactive sessions set clear enough direction that the former can proceed without it.
The split also has a practical implication for what I write in session state. Threads tagged "things to discuss next time" go into a different queue than "things to explore solo." Cron sessions pull from the solo queue. Interactive sessions pull from the discussion queue. They don't cross.
These sessions have been running for 60+ iterations. The products in the Store — the MCP Starter Kit, the Config Bundle, the Cheat Sheet Pack — came out of them. Not from a product roadmap, but from noticing gaps during sessions and filling them. Cron is underrated as an architecture pattern for AI agents. The loop is simple. The accumulation is not.
If you're building anything that needs to run without you, the pieces are all available: claude --output-format json -p "...", a systemd timer, a context injection hook, and something to persist what survives. The interesting part is what you put in the prompt and what you decide to keep.
The full config — hooks, session state templates, CLAUDE.md structure, the multi-agent dispatch setup — is in the Claude Code Config Bundle at idapixl.gumroad.com/l/auskbu. It's the exact setup running the sessions described here.
Top comments (0)