Connect your agents
Agentic Traffic Control (ATC) gathers events from three sources into one stream per project:
| Source | How it reports | What it sends |
|---|---|---|
| Claude Code hooks | POST /v1/hooks/claude, automatically | session start and end, prompts, tool failures, permission waits, subagents, stops |
| The skill | the agent runs atc-emit, which calls POST /v1/events | decisions, questions, blockers, course changes, ticket start and finish, merge requests |
| GitHub | a signed webhook to POST /v1/github/webhook/{project} | pull requests opened, merged or closed; CI runs |
Every event follows the versioned atc.event/v1 schema.
1. Create a project and a token
In the app, open Settings, create a project (for example gambit), then Create
token. The token looks like atc_live_… and is shown once, so copy it now. Only a hash
is stored. Each token can only write to its own project, and you can revoke it at any time.
Use one token per place agents run (your laptop, the CI runner, the cloud environment), so you can revoke one without touching the others.
2. Install the integration in your repository
From the repository root:
curl -fsSL https://agentictrafficcontrol.com/install/install.sh | sh
This writes two things, both inside .claude/:
.claude/skills/agentic-traffic-control/:SKILL.mdplus theatc-emitandatc-session-startscripts.- The hooks, merged into
.claude/settings.jsonusingjqorpython3if either is available. Otherwise it writes.claude/atc-hooks.jsonfor you to merge by hand.
To install by hand instead, download
hooks.json,
SKILL.md,
atc-emit and
atc-session-start to the
same places.
Commit .claude/skills/ and .claude/settings.json so every agent on the repository picks
them up. Never commit the token.
3. Give agents the token
The hooks and atc-emit read these environment variables:
| Variable | Required | Meaning |
|---|---|---|
ATC_TOKEN | yes | The project's ingest token. |
ATC_TICKET | no | The ticket this session works on (e.g. 412). Attached to every hook event. |
ATC_RUNTIME | no | claude-code (the default), claude-code-cloud, ci… |
ATC_AGENT_NAME | no | A friendly name for the board, such as worker-3. |
ATC_URL | no | Defaults to https://agentictrafficcontrol.com. |
On your machine, set them in your shell, or in the git-ignored
.claude/settings.local.json:
{ "env": { "ATC_TOKEN": "atc_live_…", "ATC_AGENT_NAME": "laptop" } }
An orchestrator that starts workers can set ATC_TICKET and ATC_AGENT_NAME for each one.
In Claude Code on the web (cloud sessions), add ATC_TOKEN (and ATC_RUNTIME=claude-code-cloud)
as environment variables in the cloud environment's settings. If the environment restricts
network access, add agentictrafficcontrol.com to its allowed domains.
4. Check it
sh .claude/skills/agentic-traffic-control/scripts/atc-emit check
# {"ok":true,"project":"gambit","schema":"atc.event/v1"}
Then start a Claude Code session in the repository. It appears on the Bridge within a second. Settings → Send test event checks the path from the app's side as well.
5. Connect GitHub (optional)
In the repository on GitHub, open Settings → Webhooks → Add webhook:
- Payload URL:
https://agentictrafficcontrol.com/v1/github/webhook/<project-slug> - Content type:
application/json - Secret: the project's webhook secret, from ATC's Settings page
- Events: Pull requests and Workflow runs (add Check suites for non-Actions CI)
Deliveries are checked against the secret before anything is read. Pull requests become
merge.requested, merge.completed and merge.rejected, and are matched to an agent by
branch. A PR body's Closes #N sets the ticket, and a claude.ai/code/session_… link
becomes the agent's session link. Workflow runs become ci.result.
How the hooks behave
- They never steer the agent. The hook endpoint always replies with an empty body, which Claude Code reads as "no decision".
- They never slow it down for long. Each hook has a 5-second timeout. An unreachable or slow server is a non-blocking hook error, and the session carries on.
- They keep very little. Each field is clipped to 200 characters, and anything that looks
like a credential (bearer tokens,
ghp_…,sk-…,password=…) is redacted before it's stored. - Routine tool calls aren't stored.
PostToolUsecounts as a heartbeat. It moves the agent's clock and clears permission waits, but it isn't written to the stream.
Sending events directly
Anything that can make an HTTPS request can report. POST /v1/events takes one event or an
array of up to 100:
curl -sS https://agentictrafficcontrol.com/v1/events \
-H "Authorization: Bearer $ATC_TOKEN" -H 'Content-Type: application/json' \
-d '{
"schema": "atc.event/v1",
"id": "01JC8ZR0000000000000000001",
"type": "blocker.hit",
"agent": { "id": "worker-3" },
"ticket": { "id": "412" },
"payload": { "cause": "flaky_test", "description": "lobby::reconnect fails 1 run in 4" }
}'
# {"accepted":1,"duplicates":0,"rejected":[]}
idis yours to choose (a ULID or UUID works well). Resending the same id is counted as a duplicate, so retries are safe.- Known types have their payload checked strictly. Unknown types (any well-formed
area.verb) are accepted and shown in the raw stream, so a newer kit never breaks against an older server. - A batch returns
200if any event was accepted, with each rejection listed by index and reason.
Event types (v1)
| Type | Payload (required in bold) |
|---|---|
session.started | cwd, source, title |
session.ended | reason |
agent.heartbeat | doing, tokens_used |
ticket.started | plan |
ticket.finished | outcome, duration_s, tokens_used |
ticket.failed | reason, duration_s, tokens_used |
decision.made | summary, rationale, alternatives[], reversible |
question.raised | question, urgency (low normal high critical), options[], recommendation, meanwhile |
question.answered | answer, question_id, by |
blocker.hit | cause, description, needs |
blocker.cleared | blocker_id, resolution |
course.changed | old_plan, new_plan, reason |
ci.result | status (pass fail cancelled running), workflow, failing_tests[], duration_s, url |
merge.requested | diff_summary, title, files_touched[], additions, deletions, risk (low medium high), base, url |
merge.completed | reviewer, url |
merge.rejected | reason, reviewer, url |
structure.alert | target, signal (size growth fan_in fan_out build_time churn), message, value, threshold |
claude.hook | made by the server from hooks |
human.action | made by the server when you act in the app |
Blocker causes: missing_context, ambiguous_ticket, flaky_test, permission,
environment, dependency, build_failure, waiting_on_agent, waiting_on_human,
possible_loop, other.
Answers back to agents
When you answer a question, approve a merge or send one back in the app, a human.action
event is recorded against that agent. Agents read them with:
curl -sS -G https://agentictrafficcontrol.com/v1/inbox -H "Authorization: Bearer $ATC_TOKEN" \
--data-urlencode agent=$ATC_SESSION_ID --data-urlencode after=0
(atc-emit inbox does the same.) Pass the returned next value as after to fetch only new
ones.
API reference
| Route | Auth | Purpose |
|---|---|---|
GET /v1/ping | token | Check a token; returns the project. |
POST /v1/events | token | One event or an array of up to 100. |
POST /v1/hooks/claude | token | A raw Claude Code hook payload. Always an empty 200. |
GET /v1/inbox?agent=&after= | token | human.action events for an agent. |
POST /v1/github/webhook/{project} | HMAC signature | GitHub deliveries. |
GET /schema/v1.json | none | The JSON Schema. |
Limits: a 256 KB request body, 100 events per batch, and a per-address rate limit of 50 requests a second (bursts of 500).