# agent.md: connect a coding agent to Hooklistener

You are a coding agent. This page gets you a token for the Hooklistener MCP server (https://app.hooklistener.com/api/mcp). The user approves one link and reads you one code; you do everything else.

Run the commands below in order; each block sets the variables it needs, so it works in a fresh shell. They need `curl` and `jq`. **Don't print the token and don't write your own scripts**: the commands keep secrets in a private file (`$HOME/.hooklistener/agent.json`) and show only what you need. On Windows, make the same requests from PowerShell (`Invoke-RestMethod`) and keep the file private the same way.

## 1. Ask for access

```bash
API="https://app.hooklistener.com/api/v1"; STATE="$HOME/.hooklistener/agent.json"
mkdir -p "$(dirname "$STATE")" && chmod 700 "$(dirname "$STATE")"
jq -n --arg agent "Claude Code" --arg project "$(basename "$PWD")" \
  '{agent: $agent, project: $project}' \
  | (umask 077; curl -sS "$API/agent/connect" -H 'Content-Type: application/json' -d @- > "$STATE")
jq '{approval_url, expires_at, error}' "$STATE"
```

Set `agent` to your own name (Claude Code, Codex, Cursor…); `project` is shown to the user next to it. Add `"scope": "read_only"` only if the user asks for it: a read-only token cannot create endpoints or inboxes.

**To keep an anonymous endpoint** you created from https://app.hooklistener.com/llms.txt, send its `id` and `claim_token`, and a name for it. Approving then moves the endpoint and everything it captured into the user's account:

```bash
API="https://app.hooklistener.com/api/v1"; STATE="$HOME/.hooklistener/agent.json"
mkdir -p "$(dirname "$STATE")" && chmod 700 "$(dirname "$STATE")"
ENDPOINT_ID="<the anonymous endpoint's id>"; CLAIM_TOKEN="<its claim_token>"
ENDPOINT_NAME="Stripe · $(basename "$PWD")"  # name it after what sends to it
jq -n --arg agent "Claude Code" --arg project "$(basename "$PWD")" \
  --arg id "$ENDPOINT_ID" --arg claim "$CLAIM_TOKEN" --arg name "$ENDPOINT_NAME" \
  '{agent: $agent, project: $project, endpoint_id: $id, claim_token: $claim, endpoint_name: $name}' \
  | (umask 077; curl -sS "$API/agent/connect" -H 'Content-Type: application/json' -d @- > "$STATE")
jq '{approval_url, expires_at, error}' "$STATE"
```

Give the user the `approval_url` and **stop until they answer**. On that page they sign in or create a free account, choose an organization and approve; the page then shows them a code like `KQPT-WMRD` to give you. The request expires 15 minutes after you made it (`expires_at`); after that, start this step again.

Errors (`error_code`): `invalid_claim` → the anonymous endpoint expired or the claim token is wrong; ask again without `endpoint_id` and `claim_token`. `invalid_scope` → fix `scope`. `rate_limited` → wait an hour.

## 2. Trade the code for the token

```bash
API="https://app.hooklistener.com/api/v1"; STATE="$HOME/.hooklistener/agent.json"
CODE="<the code the user gave you>"
RES="$(jq -n --arg t "$(jq -r .connect_token "$STATE")" --arg c "$CODE" '{connect_token: $t, code: $c}' \
  | curl -sS "$API/agent/connect/complete" -H 'Content-Type: application/json' -d @-)"
if printf '%s' "$RES" | jq -e .access_token >/dev/null; then (umask 077; printf '%s' "$RES" > "$STATE"); fi
printf '%s' "$RES" | jq '{organization, scope, endpoint, error}'
```

The token is handed over once and is now only in `$STATE`: do step 3 before anything else. When `endpoint` is set, that is the endpoint the user kept. Point senders at its `webhook_url`; the temporary URL keeps capturing into it until it expires.

Errors (`error_code`):
- `pending` → the user hasn't approved yet; ask them to approve and give you the code, then run this step again.
- `wrong_code` → ask the user to read the code again; `attempts_left` says how many tries remain.
- `expired`, `too_many_attempts`, `completed`, `not_found` → start again at step 1.
- `denied` → the user turned it away; ask them before starting again.
- `not_a_member` → the person who approved left that organization; start again.

## 3. Connect the MCP server

Use the block for the agent you are. Each writes the token into the agent's own configuration, outside the repository.

**Claude Code** (local scope: this project, on this machine; run `claude mcp remove hooklistener` first if it exists):

```bash
STATE="$HOME/.hooklistener/agent.json"
claude mcp add --transport http hooklistener "$(jq -r .mcp_url "$STATE")" \
  --header "Authorization: Bearer $(jq -r .access_token "$STATE")"
```

**Codex** (`~/.codex/config.toml`; replaces any `[mcp_servers.hooklistener]` table already there):

```bash
STATE="$HOME/.hooklistener/agent.json"
F="$HOME/.codex/config.toml"; mkdir -p "$(dirname "$F")"; touch "$F"
(umask 077
 awk '/^[[:space:]]*\[/ { skip = ($0 ~ /^[[:space:]]*\[mcp_servers\.hooklistener[].]/) } !skip' "$F" > "$F.tmp" \
   && printf '\n[mcp_servers.hooklistener]\nurl = "%s"\nhttp_headers = { Authorization = "Bearer %s" }\n' \
     "$(jq -r .mcp_url "$STATE")" "$(jq -r .access_token "$STATE")" >> "$F.tmp") \
  && mv "$F.tmp" "$F"
```

**Cursor** (`~/.cursor/mcp.json`, so the token stays out of the project):

```bash
STATE="$HOME/.hooklistener/agent.json"
F="$HOME/.cursor/mcp.json"; mkdir -p "$(dirname "$F")"; [ -s "$F" ] || echo '{}' > "$F"
(umask 077; jq --arg url "$(jq -r .mcp_url "$STATE")" --arg key "$(jq -r .access_token "$STATE")" \
  '.mcpServers.hooklistener = {url: $url, headers: {Authorization: ("Bearer " + $key)}}' "$F" > "$F.tmp") \
  && mv "$F.tmp" "$F"
```

**Any other MCP client** (streamable HTTP): the URL is `mcp_url` from `$STATE` and the header `Authorization: Bearer <access_token>`. Write them where your client keeps MCP servers, reading the token from `$STATE` without printing it. If you can't, tell the user the token is in `$STATE` (field `access_token`) and ask them to add it.

The Hooklistener tools load the next time the agent starts. The token lasts 90 days from its last use. The user can disconnect it at https://app.hooklistener.com/agents.

## 4. Leave a note for the next session

Add a short `## Hooklistener` section to the project's AGENTS.md (or CLAUDE.md, if that is what the project uses). Name what you set up and the exact steps to test it again, so a later session knows when to use Hooklistener. Never put the token in it. For example:

```markdown
## Hooklistener
Use the Hooklistener MCP server to test webhooks in this project.

- Stripe sends to https://app.hooklistener.com/w/stripe-acme-web
  (handler: src/app/api/webhooks/stripe/route.ts).
- To check a handler change: ask me to send a test from Stripe, call
  wait_for_request on stripe-acme-web, then send the captured body and headers
  to http://localhost:3000/api/webhooks/stripe with curl and report the status.
```

Tips:
- Hooklistener's servers cannot reach the user's localhost. To test a local handler, read the captured request (`get_request`, or the anonymous endpoint's events) and send it to localhost yourself.
- If the handler checks signatures, it needs the signing secret of the provider endpoint that points at Hooklistener, which is usually not the one in production.
- For signup and login emails, create an inbox (`create_inbox`), sign up with its address and read the code or link with `wait_for_email`.
