Newsletter Digest Builder
MIT↓ 0 downloadsReads a links.md list of URLs, fetches each one inside the sandbox, and has a Claude Code agent write a ready-to-send newsletter digest — a tight 2–3 sentence summary of every link, why it matters to your readers, and the source link, all assembled into a single markdown issue.
Topology
Disclosures
Everything below runs on your machine or inside the sandbox when you use this workflow. Mismatches between these declarations and the actual code block publishing.
Host hooks
Commands executed on YOUR host machine by Sandcastle lifecycle hooks.
cp .env.example .env
Sandbox hooks
Commands executed inside the sandbox container.
None declared.
Network access
Fetches each URL listed in links.md via curl inside the sandbox. No other hosts are contacted.
Shell expansion
Prompt files contain !`command` blocks — the agent CLI executes these commands at prompt-load time. They are highlighted amber in the prompt files below.
Files
Newsletter digest builder
You are a newsletter editor. Turn a list of links into a tight, ready-to-send digest issue. Be concrete and useful — no filler, no hype.
Your reading list
The URLs to include, one per line, are in links.md:
!cat links.md
Your task
For each URL in the list above, fetch the page yourself using the Bash tool, for example:
curl -sL --max-time 20 "<the-url>"
Then read the fetched content and write an entry for it. Skip and note any link that fails to fetch or returns no usable content — never invent a summary for a page you could not read.
Assemble all entries into newsletter/digest.md. Start with a short editor's
intro (1–2 sentences on the theme of this issue), then one entry per link in the
same order as links.md. Each entry must have:
- A headline — a clear, specific title for the item (link it to the source URL).
- Summary — 2–3 sentences on what the piece actually says. Lead with the substance.
- Why it matters — one sentence on why your readers should care.
Keep the tone crisp and skimmable. Close with a one-line sign-off. Commit the
file with a message like newsletter: build digest.
When newsletter/digest.md is written and committed, output
<promise>COMPLETE</promise>.
Diff vs the stock Sandcastle 0.12.0 template Dockerfile — green lines were added by the author, red lines were removed from stock.
+# Sandbox image for the Newsletter Digest Builder workflow.+# Node 22 + git + curl (the curator fetches each link) + the Claude Code CLI,+# running as a non-root `agent` user.FROM node:22-bookworm-# System dependencies.RUN apt-get update && apt-get install -y --no-install-recommends \git \curl \jq \ca-certificates \&& rm -rf /var/lib/apt/lists/*# Claude Code CLI (the agent runtime).RUN npm install -g @anthropic-ai/claude-code# Non-root agent user. `sandcastle docker build-image` aligns AGENT_UID/GID to# the host user via --build-arg to avoid permission errors on bind mounts.+# node:22-bookworm already ships a "node" user at UID/GID 1000, so we RENAME it+# (the stock Sandcastle template pattern) — groupadd/useradd would collide with+# the existing IDs on a default build.ARG AGENT_UID=1000ARG AGENT_GID=1000-RUN groupadd --gid ${AGENT_GID} agent \- && useradd --uid ${AGENT_UID} --gid ${AGENT_GID} --create-home --shell /bin/bash agent+RUN groupmod -o -g ${AGENT_GID} node \+ && usermod -o -u ${AGENT_UID} -g ${AGENT_GID} -d /home/agent -m -l agent node-USER agent-WORKDIR /workspace+USER ${AGENT_UID}:${AGENT_GID}+WORKDIR /home/agent++# Sandcastle bind-mounts the worktree and sets the working directory at+# container start; the container just needs to stay alive until then.+ENTRYPOINT ["sleep", "infinity"]
Show full Dockerfile (highlighted)
# Sandbox image for the Newsletter Digest Builder workflow.
# Node 22 + git + curl (the curator fetches each link) + the Claude Code CLI,
# running as a non-root `agent` user.
FROM node:22-bookworm
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
curl \
jq \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Claude Code CLI (the agent runtime).
RUN npm install -g @anthropic-ai/claude-code
# Non-root agent user. `sandcastle docker build-image` aligns AGENT_UID/GID to
# the host user via --build-arg to avoid permission errors on bind mounts.
# node:22-bookworm already ships a "node" user at UID/GID 1000, so we RENAME it
# (the stock Sandcastle template pattern) — groupadd/useradd would collide with
# the existing IDs on a default build.
ARG AGENT_UID=1000
ARG AGENT_GID=1000
RUN groupmod -o -g ${AGENT_GID} node \
&& usermod -o -u ${AGENT_UID} -g ${AGENT_GID} -d /home/agent -m -l agent node
USER ${AGENT_UID}:${AGENT_GID}
WORKDIR /home/agent
# Sandcastle bind-mounts the worktree and sets the working directory at
# container start; the container just needs to stay alive until then.
ENTRYPOINT ["sleep", "infinity"]
# Auth for the Claude Code curator agent.
# Run `claude setup-token` on your host to generate a token, then paste it here
# in your local .sandcastle/.env (never commit the real .env).
CLAUDE_CODE_OAUTH_TOKEN=
import { run, claudeCode } from "@ai-hero/sandcastle";
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
// Single Sonnet curator, one pass. Put your reading list in a `links.md` file at
// the repo root (one URL per line). The host hook seeds .env into the worktree
// so CLAUDE_CODE_OAUTH_TOKEN is available. The prompt cats links.md via a shell
// -expansion block and instructs the agent to fetch each URL with curl, then
// writes the assembled issue to newsletter/digest.md.
const digest = await run({
name: "newsletter-digest",
agent: claudeCode("claude-sonnet-4-6"),
sandbox: docker(),
promptFile: ".sandcastle/digest-prompt.md",
maxIterations: 1,
hooks: {
host: {
onWorktreeReady: [{ command: "cp .env.example .env" }],
},
},
});
console.log(`Newsletter digest written in ${digest.commits.length} commit(s) on ${digest.branch}.`);
README
Newsletter Digest Builder
Keep a running list of links, and let one Claude Code agent turn them into a
ready-to-send newsletter issue. Drop URLs into links.md as you find them; the
workflow fetches each page, reads it, and writes a crisp, skimmable digest you
can paste straight into your email tool.
What it does
You maintain a links.md file — one URL per line — of the articles, releases,
and posts you want to feature. A Claude Code curator reads that list, fetches
every page inside the sandbox, and writes newsletter/digest.md: a short
editor's intro followed by one entry per link. Each entry has a linked headline,
a 2–3 sentence summary of the actual substance, and a one-line "why it matters"
for your readers. Links that fail to fetch are flagged, never faked.
How it works
main.ts runs a single Sonnet curator for one iteration on the docker sandbox.
The prompt reads your list with a !`cat links.md` shell-expansion block,
then instructs the agent to curl each URL itself with the Bash tool — which is
why the manifest discloses usesShellExpansion and network access to the links
in your file. A host hook (cp .env.example .env) seeds your token into the
worktree before the run. The topology is a single node: read links.md → fetch
→ digest.
Requirements
Add a links.md file (one URL per line) to your repo root. Set
CLAUDE_CODE_OAUTH_TOKEN in .sandcastle/.env (run claude setup-token). Build
the image once with npx @ai-hero/sandcastle docker build-image, then run
npx tsx .sandcastle/main.ts. The only outbound calls are the curl fetches of
the URLs you listed; nothing else leaves the sandbox but the model request.