GitHub
Webhook adapter — PAT vs App auth, review behavior, repos, and config
Part of the channel adapters family.
Transport: webhook delivery (public URL required). Setup walkthrough: Add a channel.
Public URL required
The github adapter needs a public URL to receive webhook events. Set channels.github.webhookUrl in typeclaw.json, or configure a tunnel entry with for: { kind: 'channel', name: 'github' }. Without one, webhook registration is skipped and no events arrive. See Add a tunnel.
Own-reaction cleanup
Waiting cleanup reads every reaction page before deleting anything, matches the exact emoji and authenticated numeric actor, and deletes only that actor's matching reaction IDs. Both PAT and App authentication resolve the actor through getSelf; login aliases are not ownership evidence. A later-page error does not prove absence, and account rotation blocks the original cleanup. Replies do not await cleanup transport. Protected review acknowledgments that share the engagement emoji survive temporary cleanup and reboot.
Auth
Two modes. Both require channels.github.webhookSecret in secrets.json.
PAT mode
{
"channels": {
"github": {
"auth": { "type": "pat", "token": "github_pat_…" },
"webhookSecret": "…"
}
}
}Fine-grained PAT. The bot acts as a real GitHub user. Model-driven commands do not receive the process environment wholesale: TypeClaw brokers the PAT into one standalone, statically inspected gh invocation only when the session role has fs.see.secrets or security.bypass.medium. Lower-trust roles do not receive a long-lived PAT; configure App mode for their repository operations.
PAT brokerage deliberately excludes shapes where the destination cannot be enforced:
gh api graphqlis blocked for PATs because the query can name repositories that are not visible in argv. Use a repository-confined REST endpoint, App mode with an explicit-R owner/repomint hint, or run the PAT-backed query host-side.- A PAT explicitly declared in the agent
.envcan be brokered to one standalone GitHubgit pushfor a role entitled to use secrets. TypeClaw verifies that the live value is exactly the operator-declared value, suppresses ambient aliases, disables hooks and credential helpers, forces exact-repository TLS verification with the system CA bundle, clears local HTTP/remote proxy settings, and delivers the PAT throughGIT_ASKPASSrather than argv or Git config. Reads (clone,fetch,pull,ls-remote) remain unbrokered on the PAT path: public repositories work anonymously and private ones fail at Git's own auth. A process-only, replaced, malformed, or unauthorized PAT never silently falls through to another broad credential.gh pr checkoutremains unavailable; broad private-repository reviews use the reviewer-onlygithub_prepare_review_checkoutaction. - Chained commands, substitutions, aliases, extensions, auth/config management, token-display forms (
gh auth token,gh auth status -t, and--show-token), and other shapes that could expose the command-scoped token are rejected rather than run with broad ambient credentials. - Flag names are interpreted per command, not globally.
-Fis--fieldforgh apiandgh workflow runbut--body-fileonpr/issuecommands, so it is accepted only where it means a field; file-backed input is unavailable under every spelling.
GitHub App mode
{
"channels": {
"github": {
"auth": { "type": "app", "appId": 123456, "privateKey": "-----BEGIN RSA PRIVATE KEY-----\n…" },
"webhookSecret": "…"
}
}
}Installations are resolved per repository, and validated model-driven gh commands receive a short-lived installation token minted for that exact owner/repo. App credentials are never seeded into the ambient process GH_TOKEN; validated gh calls and runtime-owned consumers mint through the repository-scoped token bridge instead. An operator-authored GH_TOKEN remains untouched.
App-backed GraphQL requires an explicit repository mint hint (gh api graphql -R owner/repo ...); TypeClaw consumes -R, mints that repository's installation token, and removes the unsupported flag before invoking gh api. Review-producing mutations are rejected from the shell-normalized query before minting so they cannot bypass the formal-review coordinator.
Authenticated network Git (clone, fetch, pull, push, ls-remote) runs in model-driven bash under App mode as a single bare git (or the cd <path> && git … rewrite): command analysis resolves the owner/repo, mints its installation token, and delivers it only through GIT_ASKPASS env, never the command string. The command-scoped Git config also forces exact-repository TLS verification with the system CA bundle and clears HTTP/remote proxy settings so repository-local config cannot intercept the credential. Because that token would be inherited by any sibling process in a compound command, chained forms are rejected — with one exception for the common "clone then look" workflow: git clone <url> <dir> && <tail> is allowed when <tail> is not itself a git command. The clone runs with the token, then the tail is exec'd into a fresh shell under /usr/bin/env -u … that unsets every injected credential, so the inspection commands (cd, grep, cat, ls, …) never see the token. Reading canonical secret files and dumping the environment stay blocked by the independent sandbox mask and secret-exfil guards, not this broker.
Trusted-runtime GitHub CLI store
Credential precedence for git push is GitHub App, then an authoritative PAT declared in the agent .env, then the trusted GitHub CLI store. The store fallback can fund a standalone configured-remote push from a repository at any accessible path, including workspace, mounts, nested repositories, and the session /tmp; no mount-specific or typeclaw.json authorization is required.
On the host, select the account with gh auth login --hostname github.com. The next real typeclaw start or typeclaw restart automatically captures that account while ignoring ambient GitHub token variables, asks gh auth login --with-token to generate an isolated hosts.yml, and persists only that generated artifact in masked secrets.json. A typeclaw start that finds the container already running is a no-op and does not refresh credentials. On container boot TypeClaw reconstructs /home/agent/.config/gh/hosts.yml; the store remains hidden from model bash and non-bash tools and the operator's full GitHub CLI configuration is never copied. If refresh fails, TypeClaw keeps the previously persisted store and prints recovery guidance.
The store fallback still requires one configured remote with exactly one GitHub push destination. Explicit-URL pushes, --set-upstream, multiple push URLs, reads, and incomplete repository/refspec evidence do not use it; GitHub App or an authorized declared PAT may support broader push shapes.
Before resolving the store credential, TypeClaw validates and reconstructs the accepted refspec. Immediately before sandbox execution it creates a runtime-owned temporary bare repository outside the session scratch directory and mounts that clean repository read-only at a unique sandbox /tmp path. The credential-bearing Git process uses the mounted clean repository, an explicit canonical https://github.com/owner/repo.git destination, and reads the source repository's object directory as an alternate while the source worktree remains writable. Global/system config is disabled, TLS verification is forced, and proxy/helper settings are empty, so the process never loads the source repository's local Git config. The resolved token is delivered through repository-bound GIT_ASKPASS with credential.useHttpPath=true, so the helper returns a password only for the validated repository prompt. The runtime-owned temporary repository is removed by the sandbox wrapper on success, failure, or retry.
A GitHub App can't be a requested_reviewer — use its decoy user account instead.
CLI: typeclaw channel add github (interactive; offers to create a Cloudflare tunnel), typeclaw channel set github (rotate credentials).
Adapter-specific config fields
These extend the shared base config for channels.github:
| Field | Type | Default | Notes |
|---|---|---|---|
webhookUrl | string | — | Public URL for webhook delivery; tunnel used when omitted |
webhookPort | number | 8975 | Container-side port the webhook server listens on |
eventAllowlist | string[] | (12 defaults) | Events that trigger the adapter |
repos | string[] | [] | owner/name pairs; hooks auto-registered on start |
review.on | 'review_requested' | 'opened' | 'off' | 'review_requested' | When to fire a review |
review.approve | boolean | true | false downgrades APPROVE verdicts to COMMENT; a re-review that clears the bot's own block dismisses it instead |
Default eventAllowlist:
issue_comment.created
pull_request_review_comment.created
discussion_comment.created
issues.opened
pull_request.opened
pull_request.converted_to_draft
pull_request.ready_for_review
pull_request.review_requested
pull_request.review_request_removed
pull_request.synchronize
discussion.created
pull_request_review.submittedCapabilities
| Feature | Supported | Notes |
|---|---|---|
| Outbound messages | yes | |
| Reactions (add + remove) | yes | |
| Typing indicator | no | GitHub has no typing API |
| Channel-name resolution | yes | |
| Self identity | yes | |
| History | yes | |
| Attachments | no | Outbound upload and inbound byte fetch are unavailable |
| Membership | yes | |
| Review threads | yes | |
| Review state | yes | APPROVE or COMMENT, plus dismissal (see review.approve) |
Review behavior
Reviews fire on the event named in review.on (default review_requested).
- PAT mode: the bot is requested by its GitHub login.
- App mode: request the decoy user account; the App itself can't be a reviewer.
- Team-reviewer requests are honored when the bot is a member of the requested team.
Draft transitions
Converting a PR to draft immediately stops its in-progress review turn, cancels its reviewer subagent, and clears pending review rounds. A later ready_for_review starts a fresh review.
Custom eventAllowlist values must include pull_request.converted_to_draft to enable this behavior. If the allowlist currently contains no pull_request.* entry, restart after adding it so TypeClaw re-registers the hook's coarse pull_request event.
Push re-review
A push to an open PR (pull_request.synchronize) is not treated as a message. It re-checks the bot's own outstanding obligations on that PR — unresolved review threads it authored, and a standing CHANGES_REQUESTED — and wakes a session only when one is outstanding. Each unresolved thread is followed up in its own thread-scoped session, so a push and a human reply on the same thread cannot produce duplicate acknowledgements.
When a standing block also has to clear, one thread is designated the round's carrier and is alone eligible to submit the formal verdict, so concurrent siblings cannot land conflicting verdicts. Resolving a thread is never gated on carriership; it is gated only by GitHub's authoritative review state. If the carrier's turn ends without a verified verdict, another sibling is promoted. A round is bounded by a TTL and is dropped once expired or once the PR head moves on; typeclaw doctor reports a round still pending.
Requires pull_request.synchronize in eventAllowlist. It is in the shipped defaults, but a pinned eventAllowlist that omits it silently disables push re-review — doctor warns when that happens.
Webhook lifecycle
- Hooks auto-register for each entry in
repos[]on start. - On stop, only hooks created by this session are deleted.
- A ~2s delay before registration allows for Cloudflare edge warmup.
- On tunnel URL change, the adapter restarts and re-registers automatically.
Config
{
"channels": {
"github": {
"enabled": true,
"webhookUrl": "https://hooks.example.com/github",
"webhookPort": 8975,
"repos": ["acme-corp/api", "acme-corp/frontend"],
"review": { "on": "review_requested", "approve": true },
"engagement": {
"trigger": ["mention", "reply", "dm"],
"stickiness": { "perReply": { "window": 900000 } }
},
"history": {
"prefetch": {
"thread": { "head": 3, "tail": 10 },
"channel": { "tail": 10 }
}
},
"quotedReply": { "enabled": true, "queueDelayMs": 10000 }
}
}
}Credentials live in secrets.json#channels.github.