conductorv2

Integrations

GitHub

Issues, pull requests, repos, branches, commits, files, Actions workflows, releases, notifications, and code search — all accessible from your AI client via Conductor. Supports Personal Access Tokens, fine-grained PATs, and GitHub Apps.

Overview

The GitHub plugin exposes 40 tools covering every core GitHub API surface. You can ask your AI client to triage issues, open pull requests, trigger deployments, read files from any branch, and monitor Actions runs — without leaving your chat session.

Issues & PRs

Create, triage, label, comment, review, merge, and close issues and pull requests.

Repositories

List, create, fork, and configure repositories. Manage branch protection rules.

Files & Commits

Read and write files, compare commits, navigate branches and tags.

GitHub Actions

Trigger workflow_dispatch runs with custom inputs, poll run status, download artifacts.

Search

Search code, issues, and repositories across GitHub with the full query syntax.

Releases

Create and publish releases with auto-generated changelogs.

Notifications

List and mark as read your GitHub notification inbox.

Organizations

Work with org repos, teams, and members. Supports SAML SSO orgs.

Branches

Create, delete, list, and protect branches programmatically.

Authentication options

Three authentication methods are supported. Choose based on your use case and security requirements.

Personal Access Token (classic)

Easiest

A single token with broad scope-based permissions. Best for personal use, local development, and prototyping. Tokens do not expire unless you set an expiration date. The downside: scopes are coarse — you can't restrict a classic PAT to a single repo.

Fine-grained Personal Access Token

Recommended

Newer token type with per-repository and per-permission granularity. You choose exactly which repos the token can access and which permissions (read/write) it gets for each resource type. Always expires (max 1 year). Best for production personal use.

GitHub App

Teams & Orgs

A first-class GitHub entity with its own identity, higher API rate limits (15,000 req/hr per installation vs 5,000 for PATs), and fine-grained permissions. Can be installed organization-wide or on specific repos. Ideal for CI/CD, shared team tooling, and production systems. Requires an App ID, Installation ID, and a PEM private key.

Creating a classic PAT

Navigate to github.com → Settings → Developer settings → Personal access tokens → Tokens (classic) and click Generate new token (classic).

1.Set a descriptive Note (e.g. "Conductor MCP — local dev") so you can identify it later.
2.Set an Expiration — 90 days is a reasonable default. Avoid "No expiration" for production.
3.Select scopes from the table below. Start minimal and add scopes as tools return 403 errors.
4.Click Generate token and copy it immediately — GitHub shows it only once.
5.Store it securely: use conductor secret set GITHUB_TOKEN or your system keychain.
ScopeWhat it grants
repoFull access to public and private repositories — read/write code, issues, PRs, releases, branches, and deployments.
public_repoRead/write access to public repositories only. Use when you don't need private repo access.
repo:statusRead/write access to commit statuses (CI check state). Required for posting CI results.
delete_repoPermission to delete repositories. Requires repo scope too. Only include when specifically needed.
workflowRead/write GitHub Actions workflows. Required for github.actions.trigger and creating/editing workflow files.
read:orgRead organization membership, teams, and projects. Required for listing org repos and members.
write:orgWrite access to org membership and projects. Required for managing teams and inviting members.
notificationsAccess notifications. Required for github.notifications.list and github.notifications.mark-read.
gistCreate and update Gists.
read:packagesDownload packages from GitHub Packages.
write:packagesUpload packages to GitHub Packages.

For most Conductor use cases, selecting repo, workflow, and read:org covers everything. Add notifications if you want the notifications tools.

Creating a fine-grained PAT

Navigate to github.com → Settings → Developer settings → Personal access tokens → Fine-grained tokens and click Generate new token.

Resource owner

Choose your personal account or an organization. For org tokens, the org admin must approve fine-grained PATs if the org has that policy enabled. Check under org Settings → Third-party access → Personal access tokens.

Repository access

Choose All repositories for convenience or Only select repositories for tightest scope. Selecting specific repos is strongly recommended for tokens used in automation.

Expiration

Fine-grained tokens always expire. Maximum is 1 year. Set a calendar reminder to rotate before expiry. Use 90 days for automation tokens and 1 year for personal use.

Permissions

Repository permissions and account permissions are configured separately. Metadata (read-only) is always required and cannot be removed. Start with read-only and elevate to read & write only where needed.

Repository permissions

PermissionLevelsWhen you need it
ContentsRead / Read & WriteRead and write files, commits, branches, releases.
IssuesRead / Read & WriteList, read, create, update, and close issues.
Pull requestsRead / Read & WriteList, read, create, merge, and review PRs.
ActionsRead / Read & WriteTrigger workflows, read runs, download artifacts.
MetadataRead (mandatory)Basic repository info. Always required — cannot be deselected.
Commit statusesRead / Read & WriteRead and post CI check statuses.
DeploymentsRead / Read & WriteCreate deployments and update deployment status.
SecretsRead / Read & WriteManage Actions secrets. Rarely needed — include only if required.
AdministrationRead / Read & WriteManage branch protection rules, webhooks, and repo settings.

GitHub App setup

GitHub Apps provide higher rate limits, finer-grained permissions, and a distinct identity that isn't tied to any single user account. Use them for team or organizational deployments.

1.Navigate to github.com/settings/apps (for personal) or github.com/organizations/YOUR_ORG/settings/apps (for org). Click New GitHub App.
2.Set a unique App name, Homepage URL (any URL), and disable the Webhook URL if you don't need it (uncheck Active under Webhook).
3.Under Repository permissions, grant the permissions your use case needs (see fine-grained permissions table above).
4.Under Where can this GitHub App be installed?, choose Only on this account for private use or Any account for shared apps.
5.Click Create GitHub App. Note the App ID shown on the app's settings page.
6.Scroll to Private keys and click Generate a private key. Download the .pem file and move it to a safe location, e.g., ~/.conductor/github-app.pem.
7.Install the app: click Install App in the left sidebar, choose your org or account, and select which repos to grant access. Note the Installation ID from the URL after install: github.com/settings/installations/INSTALLATION_ID.

Required permissions for full Conductor access

Repository: Contents (R&W), Issues (R&W), Pull requests (R&W), Actions (R&W), Commit statuses (R&W), Metadata (R), Administration (R&W for branch protection). Account permissions: none required unless you need to list org members.

Conductor configuration

Add the GitHub plugin to your Conductor config. Use environment variables for tokens — never commit credentials to source control.

Classic PAT
# conductor.config.json
{
  "plugins": {
    "github": {
      "auth": {
        "type": "pat",
        "token": "ghp_your_personal_access_token"
      }
    }
  }
}
Fine-grained PAT
# conductor.config.json
{
  "plugins": {
    "github": {
      "auth": {
        "type": "fine-grained-pat",
        "token": "github_pat_your_fine_grained_token"
      }
    }
  }
}
GitHub App
# conductor.config.json
{
  "plugins": {
    "github": {
      "auth": {
        "type": "github-app",
        "app_id": "123456",
        "installation_id": "78901234",
        "private_key_path": "~/.conductor/github-app.pem"
      }
    }
  }
}
Environment variables (alternative)
# Using environment variables (recommended for production)
export GITHUB_TOKEN=ghp_your_personal_access_token

# Or for GitHub App auth:
export GITHUB_APP_ID=123456
export GITHUB_INSTALLATION_ID=78901234
export GITHUB_PRIVATE_KEY_PATH=~/.conductor/github-app.pem

# Conductor will pick these up automatically

Available tools

40 tools across repositories, issues, pull requests, branches, commits, files, search, releases, Actions, and notifications.

github.repo.getFetch full metadata for a repository — description, visibility, default branch, star count, topics, and license.
example input
{
  "tool": "github.repo.get",
  "input": {
    "owner": "myorg",
    "repo": "api-service"
  }
}
github.repo.listList repositories for a user or org. Filter by type (all, public, private, forks, sources, member) and sort by created, updated, or pushed.
github.repo.createCreate a new repository under your account or an organization. Set visibility, default branch, gitignore template, and auto-init.
example input
{
  "tool": "github.repo.create",
  "input": {
    "name": "new-service",
    "description": "Microservice for payment processing",
    "private": true,
    "auto_init": true,
    "gitignore_template": "Node"
  }
}
github.repo.forkFork a repository into your account or a specified organization.
example input
{
  "tool": "github.repo.fork",
  "input": {
    "owner": "upstream-org",
    "repo": "open-source-lib",
    "organization": "myorg"
  }
}
github.issue.listList issues for a repo with filters: state, labels, assignee, milestone, since date, and pagination.
example input
{
  "tool": "github.issue.list",
  "input": {
    "owner": "myorg",
    "repo": "api-service",
    "state": "open",
    "labels": ["bug", "priority:high"],
    "assignee": "octocat",
    "per_page": 50
  }
}
github.issue.getFetch a single issue by number, including its labels, assignees, milestone, and timeline summary.
github.issue.createOpen a new issue with title, body (Markdown), labels, assignees, and optional milestone.
example input
{
  "tool": "github.issue.create",
  "input": {
    "owner": "myorg",
    "repo": "api-service",
    "title": "Rate limiter not resetting after TTL expiry",
    "body": "## Description\nThe rate limiter counter is not being reset...\n\n## Steps to reproduce\n1. Send 100 requests\n2. Wait for TTL\n3. Counter remains at 100",
    "labels": ["bug"],
    "assignees": ["octocat"],
    "milestone": 3
  }
}
github.issue.updateEdit an existing issue — change title, body, state (open/closed), labels, assignees, or milestone.
github.issue.closeClose an issue with an optional close reason (completed, not_planned).
github.issue.commentPost a comment on an issue or pull request. Supports full GitHub-Flavored Markdown.
example input
{
  "tool": "github.issue.comment",
  "input": {
    "owner": "myorg",
    "repo": "api-service",
    "issue_number": 42,
    "body": "I've reproduced this locally. The TTL reset logic in\n`src/rate-limiter.ts:L87` is not awaiting the async clear."
  }
}
github.pr.listList pull requests filtered by state (open, closed, all), head branch, base branch, and sort order.
github.pr.getGet full details of a pull request including review status, merge conflict state, changed files count, and CI check rollup.
github.pr.createOpen a pull request from head to base branch. Set draft mode, reviewers, assignees, labels, and milestone.
example input
{
  "tool": "github.pr.create",
  "input": {
    "owner": "myorg",
    "repo": "api-service",
    "title": "fix: await async clear in rate limiter TTL reset",
    "body": "## Summary\nFixes #42. Added `await` to the `clearCounter()` call...\n\n## Test plan\n- [x] Unit tests pass\n- [x] Integration tests pass",
    "head": "fix/rate-limiter-ttl",
    "base": "main",
    "draft": false,
    "reviewers": ["senior-dev"],
    "labels": ["bug", "ready-for-review"]
  }
}
github.pr.mergeMerge a pull request with merge, squash, or rebase strategy. Optionally auto-delete the head branch after merge.
example input
{
  "tool": "github.pr.merge",
  "input": {
    "owner": "myorg",
    "repo": "api-service",
    "pull_number": 118,
    "merge_method": "squash",
    "commit_title": "fix: await async clear in rate limiter TTL reset (#118)",
    "delete_branch": true
  }
}
github.pr.reviewSubmit a review with APPROVE, REQUEST_CHANGES, or COMMENT. Supports inline comments on specific file lines.
example input
{
  "tool": "github.pr.review",
  "input": {
    "owner": "myorg",
    "repo": "api-service",
    "pull_number": 118,
    "event": "REQUEST_CHANGES",
    "body": "Please add a test for the edge case where TTL expires mid-request.",
    "comments": [
      {
        "path": "src/rate-limiter.ts",
        "line": 87,
        "body": "This should be `await clearCounter()` — it's async."
      }
    ]
  }
}
github.pr.commentPost a review comment on a specific line or range in a pull request diff.
github.branch.listList all branches in a repository with protection status and latest commit SHA.
github.branch.createCreate a new branch from any ref (branch name, tag, or commit SHA).
example input
{
  "tool": "github.branch.create",
  "input": {
    "owner": "myorg",
    "repo": "api-service",
    "branch": "feat/new-auth-flow",
    "from": "main"
  }
}
github.branch.deleteDelete a branch by name. Returns an error if the branch is the repo's default branch.
github.branch.protectConfigure branch protection rules: required status checks, required reviews, enforce admins, and restrict pushes.
example input
{
  "tool": "github.branch.protect",
  "input": {
    "owner": "myorg",
    "repo": "api-service",
    "branch": "main",
    "required_status_checks": {
      "strict": true,
      "contexts": ["ci/tests", "ci/lint"]
    },
    "enforce_admins": true,
    "required_pull_request_reviews": {
      "required_approving_review_count": 2,
      "dismiss_stale_reviews": true
    }
  }
}
github.commit.listList commits on a branch or path, optionally filtered by author or date range.
github.commit.getGet a commit by SHA — tree, parent commits, diff stats, and verification signature.
github.commit.compareCompare two refs (branches, tags, or SHAs). Returns the merge base, commits behind/ahead, and diff stat.
github.file.getRead file content at a path on any ref. Returns decoded content and the blob SHA for subsequent updates.
example input
{
  "tool": "github.file.get",
  "input": {
    "owner": "myorg",
    "repo": "api-service",
    "path": "src/rate-limiter.ts",
    "ref": "fix/rate-limiter-ttl"
  }
}
github.file.createCreate a new file in the repository with a commit message. Content is automatically base64-encoded.
example input
{
  "tool": "github.file.create",
  "input": {
    "owner": "myorg",
    "repo": "api-service",
    "path": "docs/rate-limiting.md",
    "message": "docs: add rate limiting documentation",
    "content": "# Rate Limiting\n\nThis service uses a sliding window...",
    "branch": "main"
  }
}
github.file.updateUpdate an existing file. Requires the current blob SHA (returned by github.file.get) to prevent lost updates.
github.file.deleteDelete a file from a branch with a commit message. Requires the current blob SHA.
github.search.codeSearch for code across GitHub using the full code search syntax: language, repo, path, extension, and content filters.
example input
{
  "tool": "github.search.code",
  "input": {
    "q": "clearCounter language:TypeScript repo:myorg/api-service",
    "per_page": 20
  }
}
github.search.issuesSearch issues and PRs org-wide or across all of GitHub. Supports all GitHub search qualifiers.
example input
{
  "tool": "github.search.issues",
  "input": {
    "q": "is:open is:issue label:bug org:myorg created:>2026-01-01",
    "sort": "created",
    "order": "desc"
  }
}
github.search.reposSearch repositories by name, topic, language, stars, and forks.
github.release.listList all releases for a repository, ordered by created date descending.
github.release.createPublish a release with tag, name, body, draft/prerelease flags, and optional auto-generated release notes.
example input
{
  "tool": "github.release.create",
  "input": {
    "owner": "myorg",
    "repo": "api-service",
    "tag_name": "v2.4.0",
    "name": "v2.4.0 — Rate limiter fix + Auth improvements",
    "body": "## What's Changed\n- fix: await async clear in rate limiter (#118)\n- feat: OAuth2 PKCE flow (#115)\n\n**Full Changelog**: https://github.com/myorg/api-service/compare/v2.3.0...v2.4.0",
    "draft": false,
    "prerelease": false,
    "generate_release_notes": true
  }
}
github.release.getGet a release by ID or tag name, including asset download URLs.
github.actions.list-workflowsList all workflow files in a repository with their IDs, names, paths, and current state.
example input
{
  "tool": "github.actions.list-workflows",
  "input": {
    "owner": "myorg",
    "repo": "api-service"
  }
}
github.actions.triggerTrigger a workflow_dispatch event with custom inputs. The workflow must define on.workflow_dispatch in its YAML.
example input
{
  "tool": "github.actions.trigger",
  "input": {
    "owner": "myorg",
    "repo": "api-service",
    "workflow_id": "deploy.yml",
    "ref": "main",
    "inputs": {
      "environment": "production",
      "image_tag": "v2.4.0",
      "notify_slack": "true"
    }
  }
}
github.actions.list-runsList workflow runs with filters: workflow file, branch, event type, status (queued, in_progress, completed), and actor.
github.actions.get-runGet full details of a workflow run including conclusion, timing, jobs URL, and triggered event.
example input
{
  "tool": "github.actions.get-run",
  "input": {
    "owner": "myorg",
    "repo": "api-service",
    "run_id": 9876543210
  }
}
github.actions.download-artifactDownload a run artifact by artifact ID to a local path. Artifacts expire after 90 days by default.
example input
{
  "tool": "github.actions.download-artifact",
  "input": {
    "owner": "myorg",
    "repo": "api-service",
    "artifact_id": 1234567,
    "output_path": "/tmp/build-artifacts"
  }
}
github.notifications.listList notifications for the authenticated user. Filter to unread only, or those you are participating in.
example input
{
  "tool": "github.notifications.list",
  "input": {
    "all": false,
    "participating": true,
    "per_page": 30
  }
}
github.notifications.mark-readMark one or all notifications as read. Pass a thread ID for a single notification, or omit to mark all.

Working with organizations

Most tools accept an owner field that can be either a personal username or an organization slug. Pass the org slug (the URL-safe name visible in github.com/ORG_SLUG) — not the organization's display name.

Org-level permissions

To list private org repos, list org members, or access org-level team data, your token needs read:org (classic PAT) or the Members account permission (fine-grained PAT). Without this, tools that enumerate org resources will return empty results rather than an error, which can be confusing. If you're seeing fewer repos than expected, check that read:org is enabled.

SAML SSO — authorizing tokens

Organizations that enforce SAML Single Sign-On require every PAT to be explicitly authorized for that org, even if the token has the right scopes. Without SSO authorization, requests to org resources return 403 Resource protected by organization SAML enforcement.

To authorize: go to github.com/settings/tokens, click your token, and look for the Configure SSO button next to the organization name. Click it and complete the SSO flow. Fine-grained PATs for SSO orgs require the org admin to approve the token first.

Org slug vs display name

Always use the org slug (login) in tool inputs, not the display name. The slug is what appears in the GitHub URL: github.com/acme-corp → slug is acme-corp. The display name might be Acme Corporation but that won't work as the owner field.

GitHub Actions integration

Conductor can trigger GitHub Actions workflows, poll for completion, and download artifacts — turning your AI client into a deployment and CI orchestration interface.

Setting up workflow_dispatch

The github.actions.trigger tool requires the target workflow to have an on: workflow_dispatch trigger. Define typed inputs to control what Conductor can pass at trigger time.

.github/workflows/deploy.yml
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]
  workflow_dispatch:           # Required for github.actions.trigger
    inputs:
      environment:
        description: "Target environment"
        required: true
        default: "staging"
        type: choice
        options: [staging, production]
      image_tag:
        description: "Docker image tag"
        required: true
        type: string
      notify_slack:
        description: "Send Slack notification on deploy"
        required: false
        default: "false"
        type: boolean

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to ${{ inputs.environment }}
        run: ./scripts/deploy.sh ${{ inputs.image_tag }} ${{ inputs.environment }}

Trigger a run

Call github.actions.trigger with the workflow filename or ID, the target branch, and any inputs defined in the workflow. The tool returns the triggered run's ID for subsequent polling.

Poll run status

Use github.actions.get-run to check a run's status and conclusion. Poll every 15–30 seconds. Status values: queued, in_progress, completed. Conclusion values: success, failure, cancelled, timed_out, skipped.

Download artifacts

After a run completes, use github.actions.list-runs to find the run, then github.actions.download-artifact with the artifact ID to download the zip to a local path. Artifacts are only available while the run is retained — GitHub defaults to 90 days.

Webhooks: GitHub → Conductor

You can configure GitHub to push events (push, issues, pull_request, release, etc.) to Conductor's HTTP webhook endpoint. Conductor validates the HMAC signature and routes the payload to a handler tool. Requires HTTP transport.

Setup
# 1. Start Conductor in HTTP transport mode
conductor mcp start --transport http --port 3000

# 2. Register the incoming webhook endpoint
conductor webhooks create \
  --name "github-events" \
  --path "/hooks/github" \
  --secret "whsec_your_32_byte_hex_secret"

# GitHub will now POST to:
#   http://your-conductor-host:3000/hooks/github

Configure webhook on GitHub

Go to your repo or org → Settings → Webhooks → Add webhook. Set Payload URL to your Conductor HTTP server address (must be publicly reachable or use a tunnel like ngrok for local dev). Set Content type to application/json. Paste your HMAC secret into the Secret field. Choose which individual events to subscribe to — avoid Send me everything to minimize noise.

HMAC signature verification

GitHub signs every webhook with your secret using HMAC-SHA256. The signature is sent in the X-Hub-Signature-256 header as sha256=<hex>. Conductor verifies this automatically. If you're handling webhooks in your own code:

Node.js — signature verification
const crypto = require("crypto");

// GitHub signs with X-Hub-Signature-256: sha256=<hex>
function verifyGitHubSignature(rawBody, signature, secret) {
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  if (signature.length !== expected.length) return false;
  return crypto.timingSafeEqual(
    Buffer.from(signature, "utf8"),
    Buffer.from(expected, "utf8")
  );
}

Recommended events to subscribe to

pushpull_requestissuesissue_commentpull_request_reviewreleaseworkflow_runcheck_rundeploymentdeployment_status

Common errors and fixes

GitHub API errors map to specific root causes. Most can be resolved by adjusting token scopes, SSO authorization, or workflow configuration.

401 Bad credentials

Cause

Token is expired, revoked, or malformed.

Fix

Generate a new PAT at github.com/settings/tokens. If using a GitHub App, check that the private key matches the app and the installation is still active.

403 Resource not accessible by personal access token

Cause

The token is valid but lacks the required OAuth scope for this operation.

Fix

Re-generate the token and add the missing scope. Common offenders: workflow (Actions), read:org (org resources), notifications, delete_repo.

404 on private repository

Cause

The token lacks repo scope, or the repository doesn't exist, or the token owner doesn't have access.

Fix

Ensure repo scope is checked when creating the PAT. For org repos, the token owner must be a member. For fine-grained PATs, add the specific repo to the token's resource access.

403 on org resources

Cause

The organization has SAML SSO enabled and the token has not been authorized for SSO.

Fix

Go to github.com/settings/tokens, click the token, then click 'Configure SSO' and authorize it for the organization.

422 Validation Failed

Cause

A required field has an invalid format, duplicate value, or violates a constraint.

Fix

Check the errors array in the response body — it lists which field failed validation and why. Common causes: duplicate branch name, invalid label name, PR head already exists.

429 Too Many Requests (rate limited)

Cause

You've hit GitHub's primary rate limit (5,000 req/hr for authenticated users) or a secondary rate limit.

Fix

Check the Retry-After response header and wait that many seconds before retrying. For sustained high volume, switch to a GitHub App (15,000 req/hr) or GraphQL API. Conductor's built-in rate limit handler will respect Retry-After automatically.

Push rejected (branch protection)

Cause

A direct push to a protected branch was attempted, bypassing required pull request reviews or status checks.

Fix

Create a branch, push to that branch, open a PR, get approvals and pass CI, then merge via github.pr.merge.

GitHub App: installation not found

Cause

The installation_id in your config doesn't match an active installation of your GitHub App.

Fix

Go to github.com/settings/apps/{app-slug}/installations to find the correct installation ID. The app must be installed on the org or repo you're trying to access.

Actions: workflow not triggerable

Cause

github.actions.trigger requires on.workflow_dispatch to be defined in the workflow YAML. If it's missing or the file doesn't exist on the target ref, GitHub returns 422.

Fix

Add the workflow_dispatch trigger to the workflow YAML and push it to the target branch before calling github.actions.trigger.

Secondary rate limit (403)

Cause

Too many concurrent requests or mutations per minute. GitHub enforces secondary limits separately from the hourly quota.

Fix

Reduce request concurrency. Space out write operations (issue creation, PR comments) with small delays between batches. Conductor's retry logic handles this automatically with exponential backoff.

Rate limits

GitHub enforces both primary (per-hour) and secondary (per-minute concurrency) rate limits. Conductor respects the Retry-After and X-RateLimit-Reset headers automatically.

APIUnauthenticatedPATGitHub App
REST API60 req/hr5,000 req/hr15,000 req/hr (installation)
GraphQL API5,000 points/hr15,000 points/hr
Search API10 req/min30 req/min30 req/min
Code Search10 req/min10 req/min
Actions (workflow triggers)500 per repo/day500 per repo/day

Secondary rate limits

In addition to the hourly quota, GitHub enforces secondary limits on concurrent requests and rapid write mutations (creating issues, comments, etc. in quick succession). These return 403 with a message like You have exceeded a secondary rate limit. Conductor handles these with exponential backoff. If you're doing bulk operations, introduce a small delay (100–500ms) between write calls to stay well within secondary limits.

Security best practices

Prefer fine-grained PATs over classic PATs

Fine-grained tokens let you restrict access to exactly the repos and permissions needed. A classic PAT with repo scope grants read/write access to all your private repos — a significant blast radius if leaked.

Apply minimal scopes

Only grant the permissions your workflow actually requires. Start with read-only and escalate to write only when a tool fails with 403. This limits damage from token exposure.

Set expiration dates

Always set an expiration on tokens — 90 days for automation, up to 1 year for personal use. Calendar a reminder to rotate them before they expire. Fine-grained PATs enforce expiration; classic PATs do not.

Never commit tokens to source control

Use environment variables or Conductor's built-in secret store (conductor secret set GITHUB_TOKEN). GitHub automatically scans repositories for accidentally committed tokens and immediately revokes them when found — but the exposure window before revocation can still be exploited.

Use GitHub Apps for team and production use

GitHub Apps have their own identity, don't rely on a user account, and survive employee offboarding. Use them for any automation that multiple people depend on.

Scope GitHub App installations tightly

Install GitHub Apps on only the repos they need, not the entire organization. Use the 'Only select repositories' option when installing.

Store private keys securely

GitHub App private keys (.pem files) are equivalent to a master password for your app. Store them in a secrets manager (AWS Secrets Manager, HashiCorp Vault, 1Password Secrets Automation) rather than on disk in plaintext.

Rotate tokens on any suspected exposure

If a token may have been exposed — leaked log, committed to a repo, sent in a message — revoke it immediately at github.com/settings/tokens and generate a new one. Don't wait for GitHub's automatic scanning.