A GitHub App with a permission labeled read-only can spend your organization's Copilot credits. GitHub's October 27, 2026 public preview lets unattended Copilot SDK services authenticate with an organization installation token instead of an employee's credentials. That gives the service an identity you can review and suspend. It also moves the spending decision into app installation approval.
Before migrating a service, trace one request from its runtime credential to the paying organization. Then check what suspension stops. This guide combines GitHub's documented controls with proposed acceptance checks; General Analysis has not tested the preview in a customer tenant.
Choose the authentication path before granting access#
For an external service, GitHub's administration guide requires an organization installation with Copilot Requests set to Read-only. It names Copilot Business and Enterprise; personal-account and enterprise installations are excluded. The installing organization's model policies and spending controls apply.
Review every permission the app requests. A service that only needs inference should not acquire repository write access merely because the same app can request it. Record its owner, installation ID, client ID, expected workloads, and other granted permissions before approval.
GitHub Actions has a different route: its built-in GITHUB_TOKEN uses copilot-requests: write and requires the Allow use of Copilot CLI billed to the organization policy. The external GitHub App route does not require that policy. Follow the SDK authentication instructions for the environment you actually run.
Keep model choice and tool authority in the review too. Our model-policy guide covers model availability; managed permissions covers supported client operations. Neither is evidence that an SDK service loaded the intended credential or that its own tools enforce authorization.
See how your AI systems hold up under real attacks
General Analysis maps AI applications and agents, red teams prompts, retrieval, tools, MCP servers, browser actions, permissions, and business workflows, then turns findings into evidence your team can reproduce and retest.
Make missing credentials fail visibly#
For a child runtime, pass the installation token through the SDK's explicit token option and set useLoggedInUser: false. That setting disables automatic discovery, including environment-token discovery. An existing runtime URI instead needs its token configured on the runtime process. GitHub's runtime examples distinguish these cases.
Check your service launcher with the intended token absent. It should stop before submitting work, even if the host has a developer login or another token available. A successful request in that condition is a migration failure: the service has found another identity. Record the selected authentication method and installation identity without logging secrets.
Installation tokens last one hour. GitHub's refresh instructions require a new SDK client for a child process, or a host-runtime restart for in-process and existing-runtime deployments. Merely replacing a secret in storage does not establish that a running process adopted it.
Plan rotation around task state. Pause new dispatch, record outstanding work, replace the credential through your supported runtime path, then resume. If a request failed during rotation, check its outcome before replaying any associated business action. That is an application recovery requirement, separate from obtaining a valid token.
Join usage to the installation without hiding gaps#
GitHub exposes app attribution in AI usage CSV exports. Match organization and app_client_id to the installation inventory, then retain the installation ID and app slug. The billing field reference distinguishes this client ID from other identifiers and explains the empty-field cases. Do not join it to numeric app_id or installation id.
An organization owner can collect GET /orgs/{org}/installations, following every page. The REST reference lists organization Administration read permission for fine-grained credentials. Use a separate inventory credential; the workload's Copilot permission is not an inventory grant.
Save each response's installations entries as one JSON array. Record the organization, capture time, page count and reported total alongside it. The reference's example response omits client_id, although the Copilot admin guide directs owners to that field. If your response lacks it, preserve the gap and confirm the mapping through the app owner; substituting a different ID creates false attribution.
The following offline Python example reads that array and the original AI usage CSV. Save it as attribute_usage.py and run python3 attribute_usage.py ORG installations.json usage.csv. It emits JSON lines, leaves source fields intact, and classifies every row belonging to the selected organization. Keep both inputs and the output private.
Code source: illustrative offline join using GitHub's billing fields and documented app attribution procedure.
import csv
import json
import sys
from collections import defaultdict
org, inventory_path, usage_path = sys.argv[1:]
index = defaultdict(list)
with open(inventory_path, encoding="utf-8") as source:
installations = json.load(source)
if not isinstance(installations, list):
raise ValueError("Expected an array containing all installation pages")
for item in installations:
owner = item.get("account", {}).get("login", "").casefold()
client_id = str(item.get("client_id") or "").strip()
if owner == org.casefold() and client_id:
index[client_id].append(item)
with open(usage_path, newline="", encoding="utf-8-sig") as source:
rows = csv.DictReader(source)
required = {"organization", "app_client_id"}
if not required.issubset(rows.fieldnames or []):
raise ValueError("AI usage export lacks attribution columns")
for line, row in enumerate(rows, start=2):
if (row["organization"] or "").casefold() != org.casefold():
continue
client_id = (row["app_client_id"] or "").strip()
matches = index.get(client_id, [])
status = (
"no_app_attribution" if not client_id else
"unmatched" if not matches else
"matched" if len(matches) == 1 else "ambiguous"
)
print(json.dumps({
"csv_line": line, "status": status, "usage": row,
"installations": [
{"id": app["id"], "slug": app.get("app_slug")}
for app in matches
],
}))
Treat IDs as opaque strings. One app can appear in multiple organizations; filtering the inventory and usage to the same organization prevents a match to the wrong payer. The script does not verify that collection included every page or prove historical installation ownership. Reconcile those facts from the collection record and retain snapshots before removing installations.
An unmatched row may refer to an app that has since been removed, an incomplete inventory, or a missing client ID. ambiguous can expose duplicated collection rows. Neither should disappear into a total labeled “known app usage.” Empty attribution is also meaningful: GitHub leaves it blank for several other authentication paths and older records. It does not mean zero usage.
The report records UTC usage dates and aggregates billing data. Use service logs for individual request outcomes and downstream systems for actions. The CSV cannot prove which prompt produced a particular write.
Set a spending boundary the organization can sustain#
The app administration documentation says there is no budget for an individual app or installation. Organization budgets apply, and an alert-only budget does not block requests. Enable the stop-usage option if the organizational limit must enforce a cutoff.
A single noisy app can consume a shared allowance. Bound task dispatch, retries and concurrent work in your service, and assign someone who can stop dispatch when consumption is unexpected. These controls limit workload volume; they do not enforce a dollar budget.
Prove suspension and runtime shutdown separately#
GitHub documents suspension or removal as the way to block one installation's Copilot access. It also blocks that installation's other access to the organization. Review those dependencies before using suspension as a routine spending control.
In Organization settings → Third-party Access → GitHub Apps → Configure, an owner can review the installation and select Suspend. GitHub's installation management guide distinguishes this from user deauthorization: suspension does not revoke members' separate authorizations, and uninstalling from one account does not remove installations elsewhere.
Use a dedicated pilot installation, harmless prompts and tools with no business side effects. These are proposed checks, not reported test results:
| Pilot condition | Evidence required before rollout |
|---|---|
| Intended token missing; developer credentials still available | Launcher fails before submission; no silent fallback. |
| Valid installation token | Service records the intended installation; exported usage resolves to its organization and app. |
| Token expires, then rotates | Expired credential fails; replacement runtime succeeds; interrupted work is reconciled before retry. |
| Installation suspended | A new request using the installation is rejected; dispatch is paused and the service process is separately stopped. |
| Alternate credentials or another installation remain | Each remaining route has its own disable decision and verification. |
Save timestamps, runtime version, installation identifiers, redacted outcomes and the inventory/report capture dates. The documentation does not establish that suspension cancels already-running agent work. Check outstanding tasks and downstream effects before closing an incident. Keep the broader coding-agent security baseline around the service's execution environment.
Attach unresolved attribution rows and shutdown failures to the rollout decision, with an owner for each. Keep the pilot contained until the team can account for them.
Frequently asked questions
- Does read-only Copilot Requests prevent an app from spending credits?
No. It authorizes Copilot requests that consume the installing organization's credits. Review the organization's budget and whether its stop-usage option is enabled. This authentication path has no individual app or installation budget.
- Does an empty app_client_id mean no app used Copilot?
No. GitHub leaves the field empty for user-attributed usage, GitHub Actions, other GitHub-owned apps, and historical usage before app attribution. Preserve those rows separately instead of assigning them to an installation.
- Does suspending a GitHub App stop all of its agent work?
Suspension blocks that installation's access to organization resources, including Copilot requests. It does not establish that the service process, queued work, other installations, or separately authorized user credentials have stopped. Verify those paths independently.

