preloop copilot: GitHub Copilot CLI through the Preloop gateway¶
Editions: OSS, Cloud, Enterprise. Unless stated otherwise, everything on this page ships in OSS.
preloop copilot starts the GitHub Copilot CLI (copilot) with BYOK
environment variables pointed at the Preloop model gateway. Interactive
mode is a TTY passthrough: stdin, stdout, and stderr stay attached, so the
session behaves like a direct copilot launch.
Model traffic goes through Preloop. GitHub-hosted models are not used for
that path. Missing copilot on PATH, a missing Preloop credential, or a
missing model alias exits with a named error and does not start
Copilot, because launching without the BYOK variables would fall through to
GitHub-hosted models.
MCP onboarding for Copilot CLI (~/.copilot/mcp-config.json) is separate.
The cloud coding agent on GitHub.com is a different surface and is not
started by this command.
Install Copilot CLI¶
Confirm it is on your PATH:
Usage¶
preloop login --token <token>
preloop copilot --model openai/gpt-5
preloop copilot --model anthropic/claude-sonnet-4-5 --provider anthropic
preloop --url https://preloop.example.com --token "$PRELOOP_TOKEN" \
copilot --model openai/gpt-5
Arguments after Preloop's own flags are passed through to copilot. Global
Preloop flags (--token, --url, -v) belong before the
copilot subcommand. Preloop reads them there and does not forward them to
copilot, so a --token value never appears in the Copilot process
arguments. Anything after copilot (other than --model and --provider)
is passed through unchanged, even if it looks like a Preloop flag.
--model¶
Gateway model alias to set as COPILOT_MODEL. Required when no enrolled
Copilot CLI managed agent has recorded a latest_model_alias (for
example before preloop agents onboard "Copilot CLI" finishes, or when
onboarding has not pinned a model). When an enrolled alias exists, it is
used unless --model overrides it.
--provider¶
Forces COPILOT_PROVIDER_TYPE to openai or anthropic. When omitted,
an alias whose normalized form starts with anthropic/ (after stripping
an optional preloop/ prefix) selects Anthropic; everything else
defaults to OpenAI. The launcher does not infer the family from product
names alone.
Environment contract¶
| Variable | OpenAI-family | Anthropic-family |
|---|---|---|
COPILOT_PROVIDER_TYPE |
openai |
anthropic |
COPILOT_PROVIDER_BASE_URL |
{PRELOOP_URL}/openai/v1 |
{PRELOOP_URL}/anthropic |
COPILOT_PROVIDER_API_KEY |
Preloop bearer credential | same |
COPILOT_MODEL |
gateway alias | gateway alias |
COPILOT_PROVIDER_API_KEY is a Preloop bearer, never a raw upstream
provider key. An explicit --token or PRELOOP_TOKEN wins. Otherwise
the launcher uses the enrolled Copilot CLI durable credential (the same
key the permission hook uses), then the saved login token.
Auth and API URL follow the rest of the CLI: --token / PRELOOP_TOKEN
/ config, and --url / PRELOOP_URL / config / https://preloop.ai.
Run Copilot CLI from flows (private runner host profile)¶
A flow can run copilot directly on a private runner, using the GitHub
Copilot login and seat of the OS user that runs preloop runner fg. This
is a separate path from preloop copilot: model traffic goes to GitHub
under that seat, not through the Preloop gateway, so it is billed as the
seat's premium requests and is not gateway metered. The execution page
shows "Not gateway metered" for these runs, and the run's
premium-request count is not a token ledger or a price. Dollars for the
seat come from the premium-request import,
per user and per day, so this path has no ticket-level cost. What it
governs, records and does not do is collected in
Copilot coverage.
- Install Copilot CLI and sign in once as the runner user
(
copilot, then/login, or setCOPILOT_GITHUB_TOKENin the runner's environment). - Add a profile to
~/.preloop/runner-host-profiles.json:
{
"profiles": [
{
"name": "copilot-review",
"executable": "copilot",
"workspace_root": "/home/example/src",
"timeout_seconds": 1800,
"model_map": {"team-default": "claude-sonnet-4.6"},
"allow_tools": ["shell(git:*)"],
"deny_tools": ["shell(git push)"],
"allow_checkout": true
}
]
}
- Restart
preloop runner fg. On the flow, choose Copilot CLI (private runner host profile), pick the private runner pool, and enter the profile name. The optional Copilot model is an alias from the profile'smodel_map; blank uses the Copilot default.
The runner starts
copilot --prompt=<prompt> -s --no-ask-user --output-format=json in a fresh
per-run directory, adds --model=<mapped model> when a model is requested,
and adds one --allow-tool / --deny-tool per profile rule. Success
requires exit zero and exactly one Copilot result event with exitCode
0. The result records the Copilot session id, the model Copilot reported
and the premium request count.
Review and implementation flows¶
A host profile can run a PR Reviewer or an implementation flow end to end:
- Checkout. When the flow enables git clone, the control plane sends
the runner a checkout plan with each lease: repository URL, branch, the
pinned commit (for a pull request, its head commit plus the refs that
reach it, such as
pull/5/head), the relative path and a read credential from the flow's tracker. The runner clones only when the profile sets"allow_checkout": true; otherwise the run fails withhost_checkout_not_allowedbefore anything is written. Repositories land in the per-run directory (workspacefor a trigger project,workspace-1,workspace-2orworkspace/<path>for configured repositories), and the prompt starts with a short note listing them. The credential is passed togitas a request header scoped to that repository URL for the clone and fetch only. It is never written to the remote URL, the git config orpending_job, and it is only sent over https (plain http is accepted only for a loopback tracker). Clonesetup_commandsare refused. - Publication (one Bitbucket Cloud repository, opt-in). A flow whose
git_clone_configsetscreate_pull_requestruns on a Copilot profile only when that profile sets both"allow_checkout": trueand"allow_publish": true. The runner then advertiseshost_publicationfor the profile. After Copilot exits successfully the runner stages and commits any changes (repository hooks are disabled), and pushes the commit to the managed branchpreloop/issue-<KEY>-<execution>with the same URL-scoped header credential it cloned with. It never force-pushes and pushes to the planned URL, not theoriginremote. The push runs from a fresh runner-owned bare repository with no global, system or checkout git config, so nothing Copilot writes to.git/configor~/.gitconfig(a proxy, CA file, URL rewrite or credential helper) can reach the credential. The control plane then opens the pull request through the bound Bitbucket tracker and binds it to the execution; the agent never supplies a pull request URL. Saving such a flow, or starting it, fails with the original "cannot publish pull requests" error when no runner in the pool advertiseshost_publication(an older CLI or a profile withoutallow_publish), and those runners never take the lease. Isolated publication, more than one repository and Cursor profiles stay refused. - Feedback continuation. Review or CI feedback on a pull request that
a Copilot host run published continues the same run: the existing
feedback thread queues a continuation, and the control plane pins it to
the runner that ran the original, which must advertise
host_continuation(same opt-in as publication). The runner resumes the Copilot session recorded from the original run's validated result with its own--resumeflag (a profile argv cannot set it), on a fresh checkout of the pull request branch, and pushes any new commit to that branch fast-forward. The pull request is looked up and bound again; no second one is opened. The continuation fails withresume_unavailable, and Copilot does not start a fresh implementation, when the session is no longer on that runner, the runner is gone or not capable, the flow's profile or Copilot model changed, or the original pull request is not confirmed. A result naming another session fails withresume_identity_mismatchand pushes nothing. - MCP tools. When the flow allows MCP tools or servers, the runner adds
a
preloop-flowMCP server to this run only (--additional-mcp-config, written to a0600file in the run directory and removed afterwards). Its token is scoped to the execution, lists only the flow's tools and is revoked when the run completes. Unless the profile setsallow_all_tools, the runner also passes--allow-tool=preloop-flow, so the flow's tools run without a prompt; profiledeny_toolsrules still apply. The PR Reviewer reads the diff and posts its review through these tools. The runner user's own~/.copilot/mcp-config.jsonservers stay available, and are governed only where they point at Preloop's/mcp/v1. - Sessions and usage. The runner exports
PRELOOP_FLOW_EXECUTION_IDandPRELOOP_FLOW_IDto Copilot. The Preloop usage hook forwards the execution id, so the Copilot session and its hook events are linked to the execution. Events a hook pushed without it are linked when the run completes, by Copilot session id. The premium requests from theresultevent are stored as one subscription row. The execution page shows "N premium requests, not metered by the gateway" and lists the linked Copilot sessions. No gateway usage row is written for the run. Those sessions, hook events and that count are execution bookkeeping; they are not token, cost or replay parity with a gateway path.
Tool permissions are local to the profile:
allow_toolsanddeny_toolstake Copilot permission rules such aswrite,shell(git:*)orgithub(get_file_contents). With no rules, any tool that needs permission, such as editing files or running shell commands, is denied because the run cannot ask. These are Copilot native-tool rules and are separate from MCP governance.allow_all_tools: truepasses--allow-all-tools. The runner refuses it unless the Preloop approval hook is installed (preloop agents onboard "Copilot CLI" --approvals), so every tool call still goes through Preloop policy.force_writes,--allow-all,--yolo,--model,--agent, prompt, resume and MCP flags cannot be set in profileargv. That includes--additional-mcp-config: the operator cannot supply or replace the flow's MCP server, and the runner passes the flag itself for every job that has one.
The Copilot environment is built from an allowlist: a per-OS system
baseline, COPILOT_*, GH_* and GITHUB_TOKEN (so the seat login is
preserved), proxy and TLS variables, and any names the profile lists in
pass_env. COPILOT_PROVIDER_*, COPILOT_OFFLINE and COPILOT_ALLOW_ALL
are removed on top of that, so a host profile always uses the seat, never a
BYOK endpoint, and the operator's unrelated environment never reaches the
run. The runner also installs the Preloop usage hooks in
~/.copilot/hooks/preloop.json (or $COPILOT_HOME/hooks) before each run,
leaving other hook files untouched; hook entries use the bash command form
on POSIX and powershell on Windows. An unchanged hooks file is not
rewritten, and a changed one is replaced atomically, so concurrent runs
never read a partial file.
Host profiles run on Linux, macOS and Windows runners; see the
Windows quickstart for npm .cmd shim
handling and command-line limits.
Named errors:
| Error | Meaning |
|---|---|
copilot_not_installed |
copilot is not on the runner's PATH. |
copilot_not_logged_in |
The runner user has no Copilot login. |
copilot_model_unavailable |
The seat does not offer the mapped model. The error lists the profile's model_map aliases; Copilot CLI has no non-interactive way to list the seat's models. |
copilot_approval_hook_missing |
allow_all_tools is set but the approval hook is not installed. |
host_checkout_not_allowed |
The flow clones repositories but the profile does not set allow_checkout. |
host_checkout_failed |
git could not clone or check out the planned commit. The error carries git's last line; the credential is never logged. |
host_publication_not_allowed |
The flow opens a pull request but the profile does not set allow_publish (and allow_checkout), or is not a Copilot profile. |
publication_failed: push_conflict |
The managed branch already exists on the remote with other commits. Nothing was force-pushed; the commit stays in the run directory. |
publication_failed: credential_rejected |
The repository refused the tracker credential for the push. Reconnect the code-host tracker with write access and run again. |
resume_unavailable |
A feedback continuation cannot resume: the Copilot session is not on the originating runner, that runner is gone or lacks host_continuation, the profile or model changed, or the original pull request is not confirmed. Nothing restarts. |
resume_identity_mismatch |
Copilot reported a different session than the one resumed; nothing is pushed. |
publication_missing |
A publishing run ended without a pushed branch, for example because Copilot made no changes. |
git_not_installed |
The flow clones repositories and git is not on the runner's PATH. |
copilot_hooks_unavailable |
Preloop could not install or read its own hooks file under ~/.copilot/hooks (or $COPILOT_HOME/hooks). The run fails before Copilot starts. |
This path does not run custom commands or clone setup commands, and does not resume sessions. Pull requests are opened only through the opt-in managed publication above (one Bitbucket Cloud repository, legacy mode). Other requests are refused before the run with a message naming the missing capability; they are not silently dropped. See host execution profiles for the shared rules, and #1069 for the open work on publication retry from retained local work and managed OAuth credential delivery, which this page does not describe as shipped.
Run flows on your team's Copilot seats¶
A flow can name the harness instead of a profile, and Preloop picks a runner whose owner has Copilot CLI installed and signed in. This works even when Preloop is not the gateway for Copilot: the run uses the seat of the runner user, so there is no API spend, and Preloop still records the run, its approvals and its audit trail.
- Each team member runs a Preloop runner with Copilot CLI installed and signed in. A runner that publishes its harness inventory reports which harnesses it has, whether each is signed in and which models it knows. Executable paths, argv, environment values and credentials never leave the host.
- On the flow, choose Copilot CLI (private runner host profile) and turn on Route by harness. The picker shows how many of the runners you may use are online and signed in, and per model how many runners list it.
- Optional: pin one runner, pick a model, and choose what happens when no runner is eligible.
The saved agent_config looks like this:
{
"harness": "copilot_cli",
"copilot_model": "claude-sonnet-4.6",
"runner_id": null,
"harness_fallback": "queue",
"harness_queue_timeout_seconds": 1800,
"fallback_model_identifier": null
}
A runner is eligible when all of these hold:
- it is online and belongs to the account;
- the flow may use it: the flow pins it with
runner_id, or names it inrunner_pool, or (for a flow that names no runner) it was registered by an account admin. A member's own runner only takes flows that name it; - for a flow that names it in
runner_pool(not pins it), the flow's author may use it: its registrant, an account admin, or aflowsshare grant. A refused runner counts as not eligible (owner_has_no_runner), soharness_fallbackapplies, includingfallback_server. Without the RBAC plugin every member is an account admin, so this only narrows routing when roles are enabled; - its inventory has
copilot_clienabled, signed in and runnable; - the model is listed, or the harness cannot list models. Copilot has no model listing, so an unlisted model is allowed and fails fast on the host when Copilot rejects it.
A runner that reports no inventory yet still takes flows pinned by
profile name, and never takes harness-routed flows. A flow that sets both
host_exec_profile and harness routes by the profile name, exactly as
before.
The lease names the harness, the profile the runner generates for it
(copilot) and the model. The runner resolves that profile locally; the
server never sends an executable or argv.
When no runner is eligible¶
harness_fallback decides:
| Value | Behavior |
|---|---|
queue (default) |
The run waits for an eligible runner for harness_queue_timeout_seconds (60 to 86400, default 1800), then fails with queue_timeout. |
fallback_server |
When the run starts and no runner is eligible, it runs on the server pool with the hosted Codex harness and fallback_model_identifier (an AI model id or model identifier of the account). That run is metered by the gateway. |
fail |
The run fails at once with the routing reason. |
While a run waits, or after it fails or falls back, the execution shows one routing reason:
| Reason | Meaning |
|---|---|
no_runner_with_harness |
No runner you may use reports Copilot CLI as runnable (or reports no inventory). |
harness_signed_out |
Copilot CLI is installed but not signed in. |
harness_disabled |
The runner owner disabled Copilot CLI for the runner. |
model_not_available |
The harness lists its models and the chosen one is not among them. |
runner_offline |
The runner that could take the run is offline. |
runner_asleep |
The runner said it was going to sleep, or a pinned runner is asleep and its offline_policy failed the run (offline policy). |
owner_has_no_runner |
No runner the flow may use exists. |
queue_timeout |
The queue timeout expired. |
fell_back_to_server |
The run moved to the server pool. |
Cost label¶
A Copilot or Cursor run on a runner is billed to the runner user's seat.
The run and the per-issue cost rollup show Seat (not metered by
gateway), with the premium requests Copilot reported when it reported
them. The rollup CSV and JSON exports carry billing_mode (seat,
metered or mixed per issue), billing_label, seat_run_count and
premium_requests, next to the existing elapsed-time columns. Seat runs
add no dollars to estimated_cost.
Copilot plan terms govern how a seat may be used. A developer running flows on their own machine with their own seat is ordinary use. Check your organization's Copilot Business or Enterprise terms before sharing one seat across automated flows for several people.
Related¶
- Copilot coverage: what each surface governs and meters
preloop cursor: Cursor Agent launcher pattern