# Claude Code as an Operations Agent: A Playbook Practices and copy-ready templates for running Claude Code as an operations agent: deploys, scheduled jobs, browser work and content publishing across a small portfolio of sites. They come from a month of real use (48 analyzed sessions, 286 hours) read through Claude Code's `/insights` report, and from fixing what it found. Adapt the paths; nothing here needs a particular host. Free to use and share. From https://jwatte.com/blog/claude-code-insights-operations-agent/ --- ## 1. Start every multi-task session with a deliverables list Long sessions that bundle five unrelated asks finish four and lose the fifth. A numbered list with a completion test keeps partial progress visible. Prompt to paste at the start: ```text Before starting, list the deliverables for this session with a one-line "done when" for each. Work on them in order. Deploy and verify each one before moving on. If we're running long, stop and write a resume kit to memory instead of starting a new item. ``` A good "done when" names an observable fact, not an effort: | Weak | Strong | |---|---| | Deploy the article | URL returns 200, the commit is on main, the URL is queued for indexing | | Fix the update script | The failing run passes again and the fix is pushed and installed | | Schedule the posts | Each post shows in the scheduler with the right date, time and link | When a new ask arrives mid-session, add it to the list instead of switching silently. ## 2. Treat memory as the handoff layer Claude Code loads `CLAUDE.md` files and its auto memory at the start of every session; auto memory loads only the first 200 lines or 25 KB of its index (see the memory docs in Sources). So: - Keep the index short: one line per topic, pointing at a detail file. - One fact or topic per detail file. Update the file; don't append a second one. - Write absolute dates, not "tomorrow". - Put rules you want followed every time in `CLAUDE.md`; put state and history in memory. Resume kit template (write it before you stop): ```markdown ## RESUME: () Done: Remaining, in order: 1. | done when | next command: 2. ... Running in the background: Blocked on the owner: Traps already paid for: ``` ## 3. Turn a recurring procedure into a skill If you type the same routine every week, make it a skill. Skills live in `~/.claude/skills//SKILL.md` (personal, all projects) or `.claude/skills//SKILL.md` (one repository). `$ARGUMENTS` receives whatever follows `/name`. Keep the skill short and point it at the detailed playbook instead of copying it. Playbooks change; a copy goes stale. ```markdown --- name: nightly-indexing description: Run the nightly sitemap resubmit + Request Indexing routine. Use for "run nightly indexing". Arguments are tonight's overrides. --- Overrides for tonight: $ARGUMENTS 1. Read the playbook at in full and last night's run sheet. 2. Check the ledger: last night logged? Which runner spends tonight? (Only one.) 3. Free work first: resubmit sitemaps, inspect each candidate, skip what is already indexed. 4. Spend only after the gate time in the run sheet; log every accept, refusal and skip as it happens. 5. On the first quota refusal: one retry after a few minutes, then stop. 6. Write the RESULT block, the next gate time, and update the memory pointer. ``` ## 4. Unattended runs: headless plus the OS scheduler A wake-up armed inside an interactive session dies when the session or the machine sleeps. Use the operating system's scheduler to start `claude -p` instead. Checklist before you arm it: - **Absolute paths everywhere.** The folder the example command `cd`s into must exist, and so must the log folder. - **Pick the working folder on purpose.** Auto memory is per project, so start the run in the folder whose memory it needs. - **Time zones.** Scheduler times are local. If your gate is in UTC, convert it, and check that it still works after the daylight-saving change. - **Permission mode.** In a `-p` run nobody can answer a prompt. If the run starts in auto mode, a bare `Bash` entry in `--allowedTools` is dropped and each command goes back to the classifier (see the headless docs). Pass the mode you mean: `--permission-mode dontAsk` runs only what you listed and denies the rest; add `--permission-prompts none` (v2.1.259 or later) so denials are final. - **Wake and limits.** Let the task wake the machine, cap its run time, and ignore a second instance. - **Dated logs.** One log per day, in UTC. - **One runner per quota.** If another machine or job can spend the same quota, turn it off or make the runs check each other's ledger. - **Validate the next morning.** Read the log and the run sheet's result block; don't assume. Windows (PowerShell), wrapper first: ```bat @echo off rem run-nightly.cmd for /f %%d in ('powershell -NoProfile -Command "(Get-Date).ToUniversalTime().ToString('yyyy-MM-dd')"') do set D=%%d if not exist "C:\ops\nightly\logs" mkdir "C:\ops\nightly\logs" cd /d "C:\path\to\the\project\folder" claude -p "/nightly-indexing" --permission-mode dontAsk --permission-prompts none --allowedTools "Bash,Read,Edit,Write,Glob,Grep" >> "C:\ops\nightly\logs\run-%D%.log" 2>&1 ``` ```powershell $action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument '/c "C:\ops\nightly\run-nightly.cmd"' $trigger = New-ScheduledTaskTrigger -Daily -At '23:40' $settings = New-ScheduledTaskSettingsSet -WakeToRun -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Hours 3) -MultipleInstances IgnoreNew Register-ScheduledTask -TaskName 'Nightly indexing' -Action $action -Trigger $trigger -Settings $settings Get-ScheduledTaskInfo -TaskName 'Nightly indexing' # read NextRunTime and convert it to UTC before you trust it ``` Linux (systemd user timer): ```ini # ~/.config/systemd/user/nightly.service [Service] Type=oneshot WorkingDirectory=%h/project ExecStart=/bin/sh -c 'claude -p "/nightly-indexing" --permission-mode dontAsk --permission-prompts none --allowedTools "Bash,Read,Edit,Write" >> %h/logs/run-$(date -u +%%F).log 2>&1' # ~/.config/systemd/user/nightly.timer [Timer] OnCalendar=*-*-* 06:40:00 UTC Persistent=true [Install] WantedBy=timers.target ``` Then `systemctl --user enable --now nightly.timer` and `systemctl --user list-timers` to see the next run. ## 5. Permissions and secrets - Pre-approve the specific commands you run every day, in `~/.claude/settings.json` (you) or `.claude/settings.json` (the repository). Use the documented rule form, for example `Bash(npx netlify deploy *)`; the space before `*` matters. - Keep rules specific. A broad `Bash` rule hands the shell to anything that runs under it. - Keep passwords, card numbers and API keys out of the conversation. Put them in a file the script reads at run time and refer to the path. Secrets pasted into a long session tend to make every later action look riskier to the safety checks. - Settings files are plain text. An `env` block holding an API key is a key in plain text; prefer a file read at run time. ```json { "permissions": { "allow": [ "Bash(npx netlify deploy *)", "Bash(npx wrangler deploy *)", "Bash(git push *)" ] } } ``` ## 6. Edits: use the edit tool, then check syntax automatically The most common breakage was patching files with `sed`, PowerShell `-replace` or JavaScript `String.replace`: backslashes and `$` sequences get mangled. Ask for edits through the Edit tool, and add a hook that checks syntax after every edit. A `PostToolUse` hook runs after the tool. Exit code 2 shows its stderr to Claude (the edit already happened, so it is a warning Claude acts on, not a block). ```json { "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "node \"/absolute/path/to/check-edited.mjs\"" }] } ] } } ``` ```js // check-edited.mjs: syntax-check edited .js/.mjs/.cjs files; exit 2 only on a real syntax error import { spawnSync } from 'node:child_process'; import { existsSync } from 'node:fs'; let raw = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', (c) => { raw += c; }); process.stdin.on('end', () => { try { const file = JSON.parse(raw || '{}')?.tool_input?.file_path || ''; if (!/\.(c|m)?js$/i.test(file) || !existsSync(file)) process.exit(0); const r = spawnSync(process.execPath, ['--check', file], { encoding: 'utf8', timeout: 20000 }); if (r.status) { process.stderr.write(`node --check failed for ${file}:\n${r.stderr}`); process.exit(2); } process.exit(0); } catch { process.exit(0); } // never block an edit because the checker itself failed }); ``` Test it once with a deliberately broken file before you rely on it. A hook that points at a script that doesn't exist fails after every edit. ## 7. Browser automation guards - **Read the current state before adding to it.** Before scheduling posts, read the scheduler's existing queue and remove duplicates by link. (We found 58 posts already queued by an earlier session; six would have been doubled.) - **Find elements, don't click coordinates, on anything that scrolls or refreshes.** A feed that shifts turns a coordinate click into a click on someone else's post. - **Guard the final click.** Before "Schedule", "Post" or "Submit", run a check that reads the exact date, time, text length, opening words and link from the page, and throw if any differ. A thrown error stops a batch before the irreversible step. - **After a misclick, verify there was no side effect** (comments, reactions, messages) before continuing. - **Small batches.** When safety checks are intermittent, one item per batch completes more often than five. After two refusals in a row, stop and save the remaining items. - **Record progress as you go:** item id, done or not, with the evidence. A resumed run must not repeat what already went out. ## 8. Long jobs and background agents - Split independent work into parallel jobs, each with its own working copy. - Watch each one: if its log has not grown for 20 minutes, check it rather than waiting. - Poll in short calls; a single hour-long wait is how a hung job goes unnoticed. - A job's report is a claim. Check its numbers yourself before you publish or deploy. ## 9. Copies drift: sync from one source of truth A security gate blocked a release because a test copy of a policy still carried acceptance dates that had expired the day before, while the owner's record had later dates. Nothing was actually wrong except the copy. - Name one source of truth for anything that exists in two places. - Sync copies from it automatically before the step that reads them. - A sync may copy; it must never invent, extend or loosen. Back up what it replaces. - When a gate still blocks after the sync, the message should name exactly what needs an owner decision. ## 10. Deploy, verify, record, then move on For each deliverable: deploy, check the live result (status 200 and a content marker, not only "deploy succeeded"), push to the repository, write the evidence where the next session will find it, and only then start the next item. Status report prompt for the start of a session: ```text Give me a status table from memory and the live state: (1) last run of each scheduled job, with its result (2) builds tested but not deployed (3) background jobs and agents still running, with their last output time (4) scheduled wake-ups with UTC times (5) items blocked on me Flag anything older than 24 hours. ``` ## 11. Read your report's suggestions critically `/insights` writes good suggestions with generic details. Before applying one: - Check every path in it exists on your machine. - Convert every time to your scheduler's time zone. - Check each flag against the current docs; behavior changes between versions. - Run the new skill, hook or task once while you watch, then let it run alone. ## Sources - Claude Code skills: https://code.claude.com/docs/en/skills - Hooks reference: https://code.claude.com/docs/en/hooks - Run Claude Code programmatically (headless, `--allowedTools`, permission modes, `--permission-prompts`): https://code.claude.com/docs/en/headless - Settings files and precedence: https://code.claude.com/docs/en/settings - Memory (CLAUDE.md and auto memory): https://code.claude.com/docs/en/memory - Windows `Register-ScheduledTask`: https://learn.microsoft.com/en-us/powershell/module/scheduledtasks/register-scheduledtask - systemd timers: https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html