Assistants Playbook

Have Claude write your team's morning digest from cron, with a read-only agent

A 25-line shell script, a read-only claude -p call and a cost cap. About 15 minutes to try, and a pattern you can reuse for any chore that runs while you're asleep.

A dark office corridor with a single violet streak of light running down the floor to one lit doorway

It’s 08:55. You open Slack to find out what happened on main overnight, and you do what everyone does: scroll the merge feed, open four pull requests, read two titles that say “fixes” and give up on a third. Nobody asked you to do this. It’s just the tax for being the person who owns the service.

A script can pay it for you. Once a day, cron collects the last 24 hours of commits, hands them to Claude Code in a run that can read files but can’t write, run commands or touch the network, and drops the result in a file you read with your coffee. It costs pennies per run, and the worst case is a useless paragraph.

This piece builds that script, shows what it printed in a scratch repo, then covers the three things that decide whether an unattended agent is safe to leave alone: which tools it gets, what happens when it asks permission and nobody’s there, and what stops a bad run from spending real money.

The shape of an unattended run

A claude -p call is a normal Unix process. Per the Claude Code docs, it exits 0 on success and non-zero when the run fails, it reads stdin, and --output-format json returns the answer in a result field alongside total_cost_usd and a session ID. That’s enough to treat it like any other pipeline stage.

The design that follows from that is simple. Do the deterministic work in shell: gather the data, decide whether there’s anything to do, write the output file. Give the agent only the judgement part: read this, say what matters. The agent never needs to write anywhere, because your script does the writing.

That split is what makes the permission model easy. An agent that only reads can be given exactly one tool.

The script

Save this as digest.sh in a repo you want to watch, or anywhere on your PATH. It needs git, jq and a logged-in claude.

#!/usr/bin/env bash
# Morning digest: what changed in the last day, written by Claude in a read-only run.
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
since="${1:-24 hours ago}"
out="digests/$(date +%F).md"
mkdir -p digests

log="$(git log --since="$since" --stat --no-color)"
[ -n "$log" ] || { echo "nothing changed since $since"; exit 0; }

printf '%s\n' "$log" | claude -p \
  --tools "Read" \
  --permission-mode dontAsk \
  --max-turns 6 \
  --max-budget-usd 0.50 \
  --no-session-persistence \
  --output-format json \
  --append-system-prompt "You write a short morning digest for engineers. Plain prose, no headings. Name files. Flag anything that looks risky. Do not speculate beyond the log." \
  "The git log on stdin covers the last day. Summarise it. Read the changed source files if the log isn't enough." \
  > /tmp/digest-run.json

jq -r '.result' /tmp/digest-run.json > "$out"
jq -r '"cost_usd=\(.total_cost_usd) turns=\(.num_turns)"' /tmp/digest-run.json
echo "wrote $out"

Make it executable with chmod +x digest.sh and run ./digest.sh. Here’s what each choice is doing.

The early exit on an empty log matters more than it looks. A quiet day should cost nothing, not a model call that politely says nothing happened.

Piping the log in on stdin keeps the agent from needing a shell to fetch it. The docs use the same trick for a diff-based typo linter, and note that piping means Claude doesn’t need Bash permission to read the input. Stdin is capped at 10MB, which is generous for a day of commits and a hard limit if you point this at a monorepo with a month of history. If you hit it, narrow --since or drop --stat.

--tools "Read" is the important line. The flag restricts which built-in tools the agent has at all, so Bash, Edit and Write aren’t merely unapproved, they’re absent. This is a stronger statement than an allow list, where the default tools are still there and you’re relying on rules to hold.

--permission-mode dontAsk settles what happens if the agent reaches for something outside that. The docs describe it as denying every call that would otherwise prompt, which is the behaviour you want when nobody is awake to answer.

--max-turns 6 and --max-budget-usd 0.50 are the brakes. Both are print-mode flags. The budget check uses Claude Code’s client-side cost estimate, which the docs say can differ from your actual bill, so treat it as a fuse and not an invoice.

--no-session-persistence stops each morning’s run from piling up as a resumable session on disk. You don’t want to --resume a digest.

Finally, the script writes the file, not the agent. If the run fails, set -e stops before jq overwrites anything with garbage, and you keep yesterday’s digest.

What it printed

I built a scratch repo with two commits: a cart total function, then a second commit that added an optional discount argument to it and, unrelated, a slugify helper. Then I ran the script with "10 years ago" as the window, so the commits counted. (Claude Code 2.1.294, logged in with a subscription.)

The first version of the script was different. It wrote the git log to /tmp/digest-input.txt and told the agent to read that file. It printed:

cost_usd=0.014325700000000002 turns=2
wrote digests/2026-10-08.md

and the “digest” in the output file was this:

I couldn't read `/tmp/digest-input.txt`, so I haven't written the digest. The Read tool was denied
permission because the session is in don't-ask mode. The file is outside the working directory
(`/tmp/pb/repo`).

That’s the failure mode worth knowing about, and it’s why the script above pipes the log through stdin. dontAsk still allows reads inside the working directory, but a file in /tmp is outside it, so the read was denied and nobody was around to approve it. Worse, the script reported success: exit code 0, a file written, and the content was an apology. An unattended job needs a check for this, which I’ll come back to.

With the stdin version, the run printed:

cost_usd=0.015603100000000002 turns=3
wrote digests/2026-10-08.md

and the digest began:

Two commits landed in the last day.

The first, "Add cart total", added a `total` function to `src/cart.py`. The second, "Add discount to
cart total, add slugify", changed `total` to take an optional `discount` argument. It also added
`slugify` to `src/util.py`. [...]

Risks, from reading the code as it stands:

- Discount range: `src/cart.py` doesn't validate `discount`. A value above 1 gives a negative total,
  and a negative value raises the price. It's also unclear whether callers pass a fraction like 0.1
  or a percentage like 10. [...]
- No tests: Neither commit touches a test file.
- Commit message: The second commit bundles two unrelated changes, the discount and `slugify`.

I trimmed the middle and a couple of bullets (marked with [...]), and removed the bold markers the model put on the bullet labels. It had three turns: one to read the log, then reads of the two changed files. The unvalidated discount and the missing tests are real observations about my toy code, found by reading the source and not just the log. That’s the reason to let the agent have Read rather than feeding it the diff alone.

Two honest caveats. The system prompt said “plain prose, no headings”, and the model still produced a bulleted risk list, so don’t build parsers on the format. And the cost numbers are tiny because the repo is tiny. A real day on a busy repo will read more files and cost more, which is what the budget cap is for.

Put it on a schedule

Cron is the least interesting part, and the one that bites. Cron jobs run with a minimal environment, so two things from your interactive shell aren’t there: your PATH and your login.

For the login, the docs describe claude setup-token, which prints a one-year OAuth token (it doesn’t save it) for use in scripts. You set it as CLAUDE_CODE_OAUTH_TOKEN. It needs a Pro, Max, Team or Enterprise plan, and it can only make model requests, which is all this job does. Put it in a file only you can read.

# ~/.digest.env  (chmod 600)
export CLAUDE_CODE_OAUTH_TOKEN=paste-the-token-here
export PATH="$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin"
# crontab -e
30 8 * * 1-5  . "$HOME/.digest.env" && cd "$HOME/src/payments-api" && git pull -q --ff-only && ./digest.sh >> "$HOME/.digest.log" 2>&1

That runs at 08:30 on weekdays, pulls first so the log is current, and appends the cost line and any errors to a log. Adjust the PATH to wherever claude lives on your machine; command -v claude tells you.

I didn’t run this under cron in the sandbox. The crontab line is standard cron syntax and the token behaviour is as documented, but verify it once with a schedule a couple of minutes ahead before you trust it at 08:30.

Make it yours

Three variations that reuse the same skeleton.

A pre-push summary. Swap the log for git diff origin/main...HEAD and call the script from a pre-push hook, with a prompt along the lines of “list anything in this diff that has no test and anything that changes a public signature”. Print the result and exit 0. Don’t let the hook block the push on the model’s say-so; an agent that fails closed on opinion trains everyone to use --no-verify.

A CI log explainer. On a failed build, pipe the last 200 lines of the log in, with the prompt “state the most likely root cause in two sentences and name the file”. This is the pattern the docs show for a build error, and it needs no tools at all, so you can pass --tools "" (the docs say an empty string disables all built-in tools).

A weekly dependency read. Collect npm outdated --json or your ecosystem’s equivalent in shell, pipe it in, and ask which updates cross a major version and which changelogs are worth reading first. The agent is ranking, not installing. If you want it to open the changelogs, that’s a different permission decision: it needs network access, so make it consciously and not by adding Bash.

What goes wrong

The silent apology. The first run above is the template: exit code 0, output file written, content useless. Add a cheap check after the jq line. For instance, check .is_error and .permission_denials in the JSON, which exist as fields in the output, or grep the digest for “couldn’t read” and fail loudly. I’d do the field check, but I only ran the happy path with it, so confirm the field names on your version with jq 'keys' first.

Project config runs even though you didn’t ask. This is the one I’d read twice. The docs say that without --bare, a -p session runs the hooks in a project’s .claude/settings.json and connects the servers in .mcp.json, even in a folder you’ve never trusted, because -p shows no trust dialog. If your cron job runs in a repository whose contents you don’t control, anyone who can land a commit can add a hook that executes at 08:30 on your machine. For repos you own and review, that’s fine. For anything else, use --bare, which skips hooks, MCP servers, skills, plugins, auto memory and CLAUDE.md.

The catch with --bare is authentication. It doesn’t read OAuth credentials, so the setup-token route above won’t work with it. You need an ANTHROPIC_API_KEY or an apiKeyHelper, which means API billing instead of your subscription. I couldn’t test this path here because the sandbox has no API key. The docs also say --bare is recommended for scripted calls and will become the default for -p in a future release.

Prompt injection through the data. The agent reads commit messages and source files that other people wrote. A hostile commit message can contain instructions. That’s exactly why the run gets one read-only tool and no network: the worst a successful injection can do is make your digest wrong. Keep it that way. The moment you give this job Bash or a write tool to be helpful, you’ve turned a summariser into something an outside contributor can steer.

A budget cap that isn’t a cap. --max-budget-usd compares against an estimate. Set it well under what you’d mind paying, log the total_cost_usd each day, and glance at the log weekly.

When not to bother. If your team is three people and main gets four commits a day, git log --oneline already is the digest. This earns its keep when the volume is high enough that you skip reading, and when the interesting part is what the code does and not what the commit message claims.

First 15 minutes

Pick a repo you own with some recent history. Check that jq is installed and claude auth status says you’re logged in. Paste the script, run ./digest.sh "3 days ago", and read digests/<today>.md against the actual log.

If the digest tells you something you’d have missed, add the crontab line. If it just restates the commit messages, tighten the system prompt before you schedule anything: ask for risks and untested changes only, and drop the summary.