1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
---
title: "DIY Codex Automations: Nocturnal Agents with Claude Code and systemd"
date: 2026-03-11T20:00:00+01:00
draft: false
slug: "diy-codex-automations-claude-code-systemd"
description: "A practical tutorial to replicate OpenAI's Codex Automations using Claude Code, systemd timers, and Gitea. Agents that work while you sleep, without relying on any desktop app."
tags: ["claude-code", "automation", "systemd", "gitea", "openai", "codex", "tutorial"]
categories: ["tutorial"]

translation:
  hash: ""
  last_translated: ""
  notes: |
    - "ñapa": means "hack/kludge/bodge". Quick and dirty fix. Not derogatory.
    - "chapuza": same as "ñapa" — a hacky solution. Translate as "kludge" or "bodge".
    - "dicho en cristiano": "in plain language". No religious connotation intended.
    - "currar": colloquial Spanish for "to work". Translate as "work" or "grind".
    - "barra del bar": "bar counter" — casual conversation metaphor.
    - "madrugón": waking up very early. Not a standard English concept — "early morning" works.
    - "irse por las ramas": "to go off on a tangent" / "to beat around the bush".
    - "otro gallo cantaría": "things would be different" / "it would be a different story".
---

Two weeks ago, OpenAI introduced Codex Automations. The idea: define a trigger (a cron job, a push, a new issue), write instructions in natural language, and an agent runs it solo in an isolated worktree. No human intervention. While you sleep, the agent triages issues, summarizes CI failures, generates release briefs, and even improves its own instructions.

Sounds like magic, right? And it is, a little. But there’s one catch they didn’t emphasize too much in the keynote: you need the Codex App running on your desktop. macOS or Windows only. No headless servers. No running it on a mini PC and forgetting about it.

And that’s when I thought: “Wait. I already have this.”

The pieces you already have

If you’re using Claude Code, you already have 90% of the infrastructure. claude --print executes a prompt without an interactive session. You give it instructions; it gives you a result and shuts down. No GUI. No open terminal. Perfect for a cron job.

If you have a server that’s always on (a mini PC, Raspberry Pi, or a $5 VPS), you’ve got the scheduler. systemd or cron, whichever you prefer, has been working away in the background for decades while you sleep.

And if you use Gitea, GitHub, or any forge with an API, you already have a place to deposit the results: comments on PRs, new issues, or committed files.

Plainly put: Codex Automations is a pattern. Not a product. And that pattern is old news.

┌─────────────────────────────────────────────┐
│           systemd timer (every N hours)      │
│                     │                        │
│                     ▼                        │
│           bash/fish script                   │
│              │                               │
│              ├── git pull --ff-only           │
│              ├── claude --print "prompt"      │
│              ├── parse results                │
│              ├── notify (Telegram/email)      │
│              └── git push (if changes)        │
└─────────────────────────────────────────────┘

Anatomy of an Automation

All automations follow the same structure. A script that:

  1. Updates the repo (git pull)
  2. Executes Claude Code in non-interactive mode
  3. Does something with the results
  4. Notifies and/or commits changes

Let’s build the first one. After that, the rest are just variations of the same theme.

The Base Script

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/usr/bin/env bash
set -euo pipefail

REPO_DIR="/srv/social-publisher"
LOG_DIR="/var/log/automations"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)

cd "$REPO_DIR"
git pull --ff-only

RESULT=$(claude --print \
  --model sonnet \
  --max-turns 3 \
  "$1")  # The prompt comes as an argument

echo "$RESULT" > "$LOG_DIR/$TIMESTAMP.md"

That’s it. The skeleton fits into 12 lines. The rest is about deciding which prompt to pass and what to do with $RESULT.

The systemd Timer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# /etc/systemd/system/claude-automation.timer
[Unit]
Description=Claude Code automation

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target
1
2
3
4
5
6
7
8
9
# /etc/systemd/system/claude-automation.service
[Unit]
Description=Claude Code automation runner

[Service]
Type=oneshot
User=claude-runner
ExecStart=/opt/automations/review-prs.sh
Environment=ANTHROPIC_API_KEY=<your-key>
1
sudo systemctl enable --now claude-automation.timer

At 3 a.m., systemd kicks off the script. Claude analyzes whatever you ask it to and deposits the result. You find out in the morning.

Example 1: Automatic PR Review

This is the most useful one. Every time there’s an open PR, Claude reviews it and leaves a comment.

Using a webhook is more elegant, but a cron job every 30 minutes works just as well for small teams:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/usr/bin/env bash
set -euo pipefail

GITEA_URL="https://git.example.com"
GITEA_TOKEN="$(op read 'op://DEV/Gitea/token')"
REPO="myorg/myrepo"

# Get open PRs
PRS=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
  "$GITEA_URL/api/v1/repos/$REPO/pulls?state=open" \
  | jq -r '.[].number')

for PR_NUM in $PRS; do
  # Get the diff
  DIFF=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
    "$GITEA_URL/api/v1/repos/$REPO/pulls/$PR_NUM" \
    -H "Accept: application/diff")

  # Claude reviews the diff
  REVIEW=$(claude --print \
    --model sonnet \
    --max-turns 1 \
    "Review this PR diff. Flag potential bugs, \
     security issues, and specific areas for improvement. Be concise. \
     Do not repeat the code; highlight issues with their line.

     $DIFF")

  # Post as a comment
  curl -s -X POST \
    -H "Authorization: token $GITEA_TOKEN" \
    -H "Content-Type: application/json" \
    "$GITEA_URL/api/v1/repos/$REPO/pulls/$PR_NUM/comments" \
    -d "{\"body\": \"## Automated Review\\n\\n$REVIEW\"}"
done

Each morning when you open Gitea, every PR has a comment with feedback. It doesn’t replace a human review, but it filters out the obvious: typos, unused imports, an if without an else that smells like a bug.


[rest of the examples and entire blog follow translated…]