Your team gets GitHub access through a GitHub App that the owner creates in the team's own GitHub organization. Tico holds no shared token: for every run Tico asks GitHub for a token that works on that bot's own repository and its effective repository grants and expires within the hour. Without a connected app nothing changes; bots keep using whatever git access their computer already has.
| Permission | Level | Why |
|---|---|---|
| Contents | write | a bot pulls and pushes its own repository |
| Pull requests | write | a bot opens pull requests for its work |
| Issues | write | a bot files and comments on issues |
| Metadata | read | required by GitHub for every app |
| Administration | write, optional | create bot repositories and delete repositories the Owner requests; omit it by leaving the box unchecked |
The app is private, receives installation change webhooks to refresh the repository list when its webhook is active, and requests no
workflow permission, so a bot cannot change .github/workflows files. Add Workflows: write on the
app's GitHub settings page if a bot's repository needs that.
Run tokens carry write permissions for write grants and contents read for read grants. Mixed grants use separate tokens; see Repositories. Tico checks the bot's own repository and effective grants, and refuses a bot the caller does not run or a repository outside the connected organization.
- As the owner, open Tools, GitHub. Enter the organization, optionally rename the app, and choose whether Tico may create bot repositories. Select Connect GitHub.
- GitHub shows the app to create. Confirm it. GitHub returns to Tico, which stores the app's credentials and sends you to install the app on the organization.
- On the install page, choose All repositories (recommended). Bots then get new repositories automatically, with nothing to add each time a bot is created. This does not widen what a bot can touch: each bot's token names its own repository plus its repository access (below), and nothing else. Choosing selected repositories works too, but every new bot repository, and every extra repository, must be added to the installation by hand. Settings then shows the app as installed.
If the organization also holds sensitive code, create a separate GitHub organization just for bot repositories and connect that one. "All repositories" then covers only what bots should reach, and a mistake in a token's scope cannot expose the rest. The app itself can reach whatever it is installed on; the per-bot limit is enforced by Tico when it asks GitHub for a token.
Bots' repositories must live in the connected organization. A bare repository name is completed by
TICO_GITHUB_OWNER, then by the connected organization.
Tick the team's repositories in Settings → Repositories, then choose own only, all ticked, or chosen repositories for each bot. Grants can be read or write. Existing extra repositories become chosen write access automatically. See Repositories for settings, tokens, tools and the legacy API alias.
With administration allowed, the owner can create <org>/bot-<slug> privately from a template:
hub bot repo-create botops # from ticoteam/botops
hub bot repo-create sales --template <org>/bot-template
The BotOps bot (bot:botops) may make the same call when the connected app permits repository creation.
To delegate this to an engineering bot or another selected bot, an Owner or admin enables
Create bot repositories in that bot’s Settings → Repositories. Other bots default to off;
BotOps keeps its built-in access. The grant can be revoked there and does not grant deletion or
broaden the bot’s normal repository access. A bot without it should ask BotOps to create the
repository, rather than changing its own permissions. It is limited to the name bot-<slug> for a bot that exists (planned or
active, not archived), always private, in the connected organization, from the default template or
one in that organization. Every creation is audited (github.repo_created) with the acting bot.
Anyone else gets 403 with the reason. Without the administration permission the command says so and
how to create the repository by hand. If the app is installed on selected repositories only, add the
new repository to the installation.
An Owner can create an exact product repository without the bot- prefix from Settings → Repositories.
Enter one repository name, review the connected organization, exact name, and private/empty settings, then
select Create private empty repository. The API also exposes this flow to the Owner's Hub CLI:
hub repo product-create tico-recorder
The command prints the same preview and requires typing the exact org/name before it writes. Both flows
create with private: true and auto_init: false, so GitHub receives no initial commit and existing local
history can be published separately. The write is audited as github.product_repo_created and retries with
the same request key replay the saved receipt. This path is Owner-only; it does not alter the bot-prefixed
creator or grant repository access to any bot.
Before sending the create request, Tico durably binds the Owner, operation key, request digest, and exact
organization/name. If GitHub's response is lost, times out, or returns a server error, the API returns
409 github_create_outcome_unknown; a retry with different content gets 409 idempotency_conflict, and a
retry with the same content reports the unresolved outcome without issuing another create. Check the
connected organization directly before taking another action. Tico does not infer that an existing
repository belongs to the unresolved operation, adopt it, or retry that external write automatically.
The operation binding and completed receipt outlive the generic idempotency cache.
Creating the empty repository does not publish source. Publishing a preserved source bundle is a separate,
Owner-authorized step: restore or use the preserved checkout, verify its expected branch and history, and
push normally to the exact new org/name using a credential authorized for that repository. Do not reset
the preserved checkout or force-push. If the destination already has refs, the push is non-fast-forward,
or the source history does not match the reviewed bundle, stop and have an Owner reconcile it before any
write. If the GitHub App uses selected repositories, an Owner must separately add the new repository in
GitHub installation settings before Tico can access it; Tico does not change that selection.
The stable API is GET /api/v2/github/product-repos/preview?name=tico-recorder, followed after review by
POST /api/v2/github/product-repos with an Idempotency-Key and this body:
{"org":"<connected org>","name":"tico-recorder","visibility":"private","auto_init":false,"confirmed":true}The preview reports the exact org/name plus one capability state: available, missing, unknown,
or not_installed. If the app is not connected, the preview returns 409 github_not_connected instead
of claiming a target organization. Creation returns the repository URL and whether the selected installation
can access it. The server rejects names containing an organization or URL and revalidates the preview's
organization at write time.
Before confirmation, Tico reads the connected GitHub App installation's live permissions. It reports whether Administration: write is available, missing, or could not be verified; a stored setup choice alone is not treated as proof. If the installation uses selected repositories, the result says when the new repository is not yet accessible. An Owner must add it in GitHub App installation settings and then refresh Settings → Repositories. Tico never changes the installation's repository selection or bot access grants.
A bot BotOps built locally (no GitHub yet) has history to keep, so it needs an empty repository, not
a template copy. --empty (API {"slug": ..., "empty": true}, no template) creates a private
<org>/bot-<slug> with nothing in it (POST /orgs/<org>/repos, auto_init false). Then, from the
bot's checkout:
hub bot repo-create <slug> --empty
Then set the bot's repository (Settings, Bots) to <org>/bot-<slug>; a bare bot-<slug> there also
works and means the connected organization (an existing emp-<slug> repository keeps its name). Nobody pushes by hand: a run's token
(POST /api/v2/github/token {"bot": "<slug>"}) is scoped to that bot's own repository, so BotOps
cannot push another bot's history. Instead the runner does it in the bot's own run, at the start
and again after a completed run: when the checkout has commits but no upstream, it sets origin to
the resolved https URL and runs git push -u origin <branch> with that bot's token. It never forces.
If the repository already has history the checkout does not contain, or origin points somewhere
else, nothing is pushed and Health shows "Bot history" with the reason (the bot's warning in
Settings, Bots says the same); fix the cause and the next run publishes.
The Team Owner can delete a repository in the connected organization with
hub api DELETE github/repos/<owner>/<repo>, or ask BotOps to do it with the Owner's rights.
This runs directly and uses a repository-scoped GitHub App token with Administration (write).
The App must have access to that repository and the installation must accept Administration permission.
Tico records the exact repository in its history. Success returns deleted: true; a 404 under a
limited identity does not prove deletion. Removing a bot uses archive and preserves its history;
it does not delete its repository.
The app's private key, client secret and webhook secret are encrypted (AES-GCM) in the Tico database.
With TICO_CREDENTIAL_KMS_KEY set the key is the credential vault's KMS-wrapped data key; otherwise
it is a random github-app.key (mode 0600) beside the database, so a database copy alone does not
carry the app's key. No API returns the key and it is never logged. Installation tokens are cached in
server memory only, until five minutes before they expire. The runner holds the run's token in the
run's process environment (GH_TOKEN, and an inline git credential helper); nothing is written to disk.
A token is fixed for its run, and a run may outlast it only after about fifty minutes of the hour.
On the app's GitHub settings page (Settings, Developer settings, GitHub Apps, your app), generate a new private key and delete the old one. Then disconnect in Tico and connect again, or delete the app and reconnect, so Tico stores a key GitHub still accepts. Cached tokens keep working until they expire.
Disconnect in Tico only forgets the app locally; bots fall back to their computers' git access. To revoke access on GitHub, uninstall the app from the organization's installed apps page, or delete the app from its settings page.
New GitHub Apps include the task events and read permissions automatically. For an existing
App, enable webhook events for Pull requests, Pull request reviews,
Pull request review comments, Check runs, Check suites and Commit statuses, plus Push for
release tracking. The webhook remains POST /api/v2/github/webhook with signature verification.
Checks need read access to Checks and commit statuses need read access to Commit statuses in
the GitHub App. Existing installations without these events keep their last known PR states;
opening a task refreshes reachable PRs, cached for three minutes.
For a manually configured App, open GitHub → Settings → Developer settings → GitHub Apps →
your App. Under Webhook, check Active. Under Permissions & events → Subscribe to events,
check Push and save. Keep the existing webhook URL (https://<your-host>/api/v2/github/webhook),
secret, SSL verification and repository access unchanged. In Recent Deliveries, confirm a genuine
event receives HTTP 200. An installation event confirms delivery, but does not restore missed pushes;
enabling Push does not recreate historical deliveries.
A PR event updates every task linked to that PR. Checks record passing, failing or pending; PR updates record clean, conflict or unknown mergeability. Reviews record approved, changes requested or commented; review comment creation/deletion updates the pending count. Commit statuses are matched to the PR's last reported head commit. The owner receives the specific failed check, new conflict, request for changes, comment from someone else or PR closed without merging, with events per task grouped into one wake within three minutes. Passing and pending checks and the bot's own comments never wake it. Requests for changes always wake it. A reviewer who pushes to a bot's PR remains a reviewer; only a pusher matching the PR author counts as the bot. New heads reset mergeability to Unknown, and conflicts wake once per head until a clean result clears the marker. Check suites are tracked separately by App. The grouping survives server restarts and keeps at most 50 distinct notice items per burst.
Several PRs can belong to one task. Automatic Ready requires every tracked PR merged or closed and at least one merge. Tracking applies to repositories in the connected org that are reachable, ticked or have received a PR webhook on any task. Without a GitHub App, a new repository counts as tracked only after its first PR webhook. Until then, the first task linking that repository can reach Ready before all of its PRs finish. Abandoning every PR returns Review or Ready to Doing. A human can always move a task to Ready or Done, and GitHub preserves their choice for one hour. A release completes it only after all merged PRs are included; merged PRs in another repository remain Ready. Automatic completion waits for open subtasks. When push deliveries were missed, the enabled background scheduler also checks up to 20 merge/deployed commit pairs every three minutes using the existing App's Contents read access. It compares against the running release's recorded commit, never latest main or the newest tag. Only verified same-repository ancestry completes a task; errors, missing commits and diverged histories leave it Ready. Failed checks retry after at least five minutes. Results are cached in memory for up to 512 immutable pairs and checked again after a restart; completion records the verified pair. All linked PRs, open subtasks and the one-hour human-change protection still apply. Completion means Done, not Closed, and creates no push history. Apps created before v0.3.1 may have Webhook → Active turned off in GitHub App settings. Their repository list refreshes daily and on Refresh in Settings → Repositories. The App webhook API does not expose the Active switch; enable it in GitHub to receive installation events.