Files
tiennm99 5c5463057e docs: point implementation cross-references at in-repo directories
The three deployment variants now live in one repository: GitHub Actions
at the root, the Go daemon under self-hosted/, the Worker under
cloudflare-worker/. Update comparison tables, clone instructions, and the
Go module path (binary builds as 'self-hosted'); GHCR image name is
unchanged.
2026-08-10 15:54:39 +07:00

131 lines
5.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# claude-code-routine-cron
Self-hosted Go daemon that fires a [Claude Code routine](https://code.claude.com/docs/en/routines) on a precise cron schedule via the [`/fire` API](https://platform.claude.com/docs/en/api/claude-code/routines-fire). Packaged as a multi-arch Docker image on GHCR.
> [!TIP]
> **Anthropic's routine editor now ships a built-in cron trigger** — that runs on Anthropic's infra, no setup. Use it first.
>
> This repo is for users who explicitly want **self-hosted scheduling**: precise timing (no GitHub Actions delays), behind-firewall, on-prem audit trail, or to integrate with their own infra.
## Why this vs `claude-code-routine-trigger`
| | [GitHub Actions](../) (repo root) | **self-hosted** (this directory) |
| ---------------- | -------------------------------------------------------------------------------------- | ---------------------------------------- |
| Runs on | GitHub Actions runners | your infra (Docker host, k8s, NAS, RPi) |
| Cost | free (within GitHub minutes) | minimal (your infra) |
| Cron precision | ±30 min – 2 h, can drop runs | sub-second |
| Setup | fork + 2 repo secrets | 2 env vars + cron + Docker |
| Secret storage | GitHub repo secrets | host env / Docker secrets / k8s Secret |
| Audit trail | GitHub Actions runs page | container stdout |
## Quickstart
```bash
docker run -d --name claude-routine \
--restart unless-stopped \
-e ROUTINE_FIRE_URL='https://api.anthropic.com/v1/claude_code/routines/trig_.../fire' \
-e ROUTINE_FIRE_TOKEN='sk-ant-oat01-...' \
-e CRON_SCHEDULE='0 9 * * *;0 18 * * *' \
-e TZ='Asia/Ho_Chi_Minh' \
ghcr.io/tiennm99/claude-code-routine-cron:latest
```
Tail logs:
```bash
docker logs -f claude-routine
```
A successful fire logs a JSON line with `claude_code_session_url`. Open it to watch the run.
## Environment variables
| Name | Required | Default | Notes |
| -------------------- | :------: | ---------------------------------------- | ----- |
| `ROUTINE_FIRE_URL` | yes | — | Anthropic `/fire` endpoint. From routine editor → API trigger. |
| `ROUTINE_FIRE_TOKEN` | yes | — | `sk-ant-oat01-...` per-routine token. Shown once in the editor. |
| `CRON_SCHEDULE` | yes | — | One or more standard 5-field cron expressions. Split on `;` or newlines. |
| `TZ` | no | `UTC` | IANA tz name (`Asia/Ho_Chi_Minh`, `America/New_York`, …). Cron evaluates in this zone. |
| `TEXT_TEMPLATE` | no | `Scheduled trigger at {{.LocalTime}}` | Go [text/template](https://pkg.go.dev/text/template); see *Templates*. |
| `LOG_LEVEL` | no | `info` | `debug`, `info`, `warn`, `error`. |
Validation is fail-fast: missing required vars or malformed crons/timezones/templates cause the daemon to exit with a clear error before any HTTP traffic.
## Multiple schedules
Use `;` (or newlines, in YAML block scalars) to register multiple crons:
```yaml
environment:
CRON_SCHEDULE: |
0 9 * * *
0 13 * * *
0 18 * * *
```
Each schedule fires independently; all use the same `TEXT_TEMPLATE`, URL, and token.
## Templates
The `TEXT_TEMPLATE` env var is rendered as a Go `text/template` per fire with these variables:
| Var | Type | Example |
| ------------ | ----------- | -------------------------------- |
| `.Now` | `time.Time` | UTC time of the fire |
| `.LocalTime` | `string` | `2026-05-08 23:30 +07` |
| `.Cron` | `string` | `0 9 * * *` — the expression that fired |
Example:
```bash
-e TEXT_TEMPLATE='Daily digest at {{.LocalTime}} (cron {{.Cron}})'
```
## docker-compose
See [`docker-compose.example.yml`](./docker-compose.example.yml) and [`.env.example`](./.env.example):
```bash
cp docker-compose.example.yml docker-compose.yml
cp .env.example .env # then edit secrets
docker compose up -d
```
## Security
- The token is **per-routine**: a leak only fires that one routine.
- Token never appears in logs (verified by tests).
- No retry on failure — each `/fire` POST creates a new session, retries would multiply sessions and burn quota.
- Image is `gcr.io/distroless/static-debian12:nonroot` — no shell, no package manager, runs as UID 65532.
- TLS to `api.anthropic.com` uses standard Go cert verification.
## Beta header
The request pins `anthropic-beta: experimental-cc-routine-2026-04-01`. When Anthropic ships a new dated beta, bump it via a new release. Older dated values keep working for a transition window per Anthropic's beta policy.
## Operational notes
- **Time accuracy**: cron correctness depends on the host clock. Run NTP / sync on the Docker host.
- **Restart policy**: use `--restart unless-stopped` (or k8s `Deployment`) — the daemon does not self-restart on panic.
- **Pin tags in production**: prefer `:vX.Y.Z` over `:latest`.
- **No idempotency**: avoid retry loops in upstream automation that POSTs the same alert twice.
## Build from source
```bash
git clone https://github.com/tiennm99/claude-code-routine-trigger
cd claude-code-routine-trigger/self-hosted
go build .
./self-hosted # uses env vars from your shell
```
Tests:
```bash
go test -race -cover ./...
```
## License
[Apache-2.0](./LICENSE)