# GitHub Copilot Tool-Call Control & Audit — Install Guide

Install ACP for GitHub Copilot — Copilot CLI and VS Code agent mode — through their native hooks. Every tool call, shell, file edits, MCP, is policy-checked before it runs and recorded after. One command, one file: ~/.copilot/hooks/acp.json. Plus the repo-level copy that governs every contributor.

# Govern GitHub Copilot with Agentic Control Plane

<p style="font-size:17px;line-height:1.6;color:var(--acp-text);max-width:660px;margin:8px 0 6px;">See, control, and price <strong>every tool call GitHub Copilot makes</strong> — shell, file edits, MCP — from one dashboard, whether it runs in Copilot CLI or in VS Code's agent mode. Copilot reads <a href="/controls/copilot">Claude Code's hook contract</a>, so this is the same hook, one file.</p>

Copilot is the harness most teams already have, because it came with the GitHub plan. It is also why mixed fleets are the norm: the person next to you runs Codex or Claude Code, the pull request comes from the cloud coding agent, and the policy that says *no force-push, no touching `.env`* has to hold across all of them. ACP puts one policy and one record under every harness; this page is the Copilot part.

*For Copilot's own approval model — prompts, allow/deny flags, VS Code's permission levels, the sandbox — see [the controls page](/controls/copilot). For the cross-harness picture, [the controls comparison](/controls).*

## TL;DR



The installer detects Copilot CLI (`~/.copilot` or `copilot` on your PATH) or the Copilot Chat extension in VS Code, writes `PreToolUse` and `PostToolUse` hooks to `~/.copilot/hooks/acp.json`, and opens your browser once to provision a workspace and save the key to `~/.acp/credentials`. Restart Copilot CLI, or reload VS Code. Every tool call is governed from the next session.

**Prefer fully on-device?** Add `--local` — no browser, no account, nothing leaves your machine. Decisions are made by `~/.acp/decide.mjs` against `~/.acp/policy.json`, with an always-on safety floor, and every call is logged to `~/.acp/audit.jsonl`. Re-run without `--local` any time to upgrade.

## What the installer writes

```json
{
  "version": 1,
  "hooks": {
    "PreToolUse":  [{ "type": "command", "command": "env ACP_CLIENT=copilot ACP_HARNESS=copilot node $HOME/.acp/govern.mjs", "timeout": 5, "timeoutSec": 5 }],
    "PostToolUse": [{ "type": "command", "command": "env ACP_CLIENT=copilot ACP_HARNESS=copilot node $HOME/.acp/govern.mjs", "timeout": 5, "timeoutSec": 5 }]
  }
}
```

Three things about this file are deliberate. The event names are PascalCase, which selects Copilot CLI's "VS Code compatible" payload: snake_case fields, and a `tool_name` already mapped to Claude Code's vocabulary (`bash` arrives as `Bash`, `view` as `Read`, `create` as `Write`). The entries are flat, because Copilot ignores the nested `{matcher, hooks: […]}` shape Claude Code and Cursor use. And both timeout fields are present: `timeoutSec` is Copilot CLI's, `timeout` is VS Code's.

The file is ACP's alone, so re-running the installer rewrites it and `acp-uninstall` removes it by name. Hooks you keep in other files under `~/.copilot/hooks/` are never touched. On Windows the installer writes the same file with a shim path in place of `$HOME`, so it reads identically under PowerShell and cmd.

If `disableAllHooks` is set in `~/.copilot/settings.json`, the installer says so; nothing runs until it is removed.

## How it works

One registration, two dialects:

| | Copilot CLI | VS Code agent mode |
|---|---|---|
| Reads | `~/.copilot/hooks/*.json`, repo `.github/hooks/`, repo `.claude/settings.json` | The same, plus `~/.claude/settings.json` |
| Sends | `tool_name` in Claude Code's vocabulary (`Bash`, `Read`, `Write`, `Edit`, `Grep`, `Glob`, `WebFetch`, `Agent`) | Its own ids: `run_in_terminal`, `create_file`, `replace_string_in_file`, `read_file`, `grep_search`, `file_search`, `fetch_webpage`, `runSubagent`… with camelCase inputs (`filePath`) |
| Expects back | `permissionDecision` at the top level | `hookSpecificOutput.permissionDecision` |
| Renders an `ask` as | A hook-permission prompt; the human can add a reason, which is passed to the agent on a deny | A confirmation dialog |

The hook handles the difference so the policy does not have to. VS Code's ids are canonicalized before any decision, so `run_in_terminal` is governed as `Bash` and reaches the dotted policy tiers (`Bash.git.push`, `Bash.rm`) and the hardline floor the same way a Claude Code call does. Every verdict is written in both output shapes, so each reader takes its own. In the activity log, CLI calls appear as `copilot` and VS Code calls as `copilot-vscode`.

One more rule, for machines where Claude Code was wired without the plugin: VS Code also runs the hook in `~/.claude/settings.json`. When the Copilot registration exists, that second hook stands down for VS Code's calls. One governed call, one row.

Everything else is the shared ACP hook: `PreToolUse` posts the call to your workspace and returns allow, ask or deny; `PostToolUse` records the result and scans it. Copilot's own approvals run first and are unaffected. A policy that would ask you renders as Copilot's native prompt.

## Teams: the repo-level copy {#repo}

The user-level file governs one person's machine. Copilot also reads hooks committed to the repository, and this is the part that matters for a team: one file in `.github/hooks/` and every contributor's Copilot CLI and VS Code agent mode run it, on every clone, with no installer step and no MDM.

```json
{
  "version": 1,
  "hooks": {
    "PreToolUse": [{
      "type": "command",
      "bash": "if [ -f \"$HOME/.acp/govern.mjs\" ]; then ACP_CLIENT=copilot ACP_HARNESS=copilot node \"$HOME/.acp/govern.mjs\"; else echo '{\"systemMessage\":\"[ACP] not installed on this machine - this call ran ungoverned. Install: curl -sf https://agenticcontrolplane.com/install.sh | bash\"}'; fi",
      "powershell": "if (Test-Path \"$env:USERPROFILE\\.acp\\govern-copilot.mjs\") { node \"$env:USERPROFILE\\.acp\\govern-copilot.mjs\" } else { '{\"systemMessage\":\"[ACP] not installed on this machine - this call ran ungoverned. Install: irm https://agenticcontrolplane.com/install.ps1 | iex\"}' }",
      "timeoutSec": 5,
      "timeout": 5
    }],
    "PostToolUse": [{
      "type": "command",
      "bash": "if [ -f \"$HOME/.acp/govern.mjs\" ]; then ACP_CLIENT=copilot ACP_HARNESS=copilot node \"$HOME/.acp/govern.mjs\"; fi",
      "powershell": "if (Test-Path \"$env:USERPROFILE\\.acp\\govern-copilot.mjs\") { node \"$env:USERPROFILE\\.acp\\govern-copilot.mjs\" }",
      "timeoutSec": 5,
      "timeout": 5
    }]
  }
}
```

Commit that as `.github/hooks/acp.json` and merge it to the default branch. What it does, honestly:

- **On a machine where ACP is installed**, every Copilot tool call in that repo is governed by the workspace policy. Contributors who also have the user-level file get the same call through both; the hook is idempotent on the gateway side, and the user-level file is what they should keep.
- **On a machine where ACP is not installed**, the call runs and the agent is told so. VS Code shows the notice; Copilot CLI ignores fields it does not know. This is fail-open on purpose. Copilot CLI treats a crashing `preToolUse` hook as a deny, so a repo hook that assumed `~/.acp` existed would block every tool call for every contributor who had not installed it yet.
- **In the cloud coding agent**, `preToolUse` fires from `.github/hooks/` and GitHub treats an `ask` as a deny. The cloud sandbox has no `~/.acp` and no credentials, so today the repo hook passes cloud runs through with the notice. Cloud runs are recorded as not governed, not as governed.

Two things that fall out of Copilot reading Claude Code's format. A repo that already commits `.claude/settings.json` hooks for Claude Code gets VS Code agent-mode coverage from that file too; nothing to add. And the reverse: repo hooks run with no trust prompt in VS Code, which is why the ACP hook is fail-open and hash-attested, and why the notice above says what it says instead of staying silent.

Copilot Business and Enterprise administrators can disable hooks by policy. If your organization has, none of this runs, and the activity log stays empty rather than lying.

## Verified

Contract-verified 2026-09-21 against [GitHub's hooks reference](https://docs.github.com/en/copilot/reference/hooks-reference), the [Copilot CLI hooks guide](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/use-hooks) and [VS Code's agent hooks reference](https://code.visualstudio.com/docs/agents/reference/hooks-reference). The hook's own test suite exercises both dialects, both output shapes, the VS Code canonicalization, and the stand-down rule. VS Code agent hooks are a Preview feature, and GitHub's issue tracker records a Copilot CLI version in which a `preToolUse` deny was not honoured ([copilot-cli#3874](https://github.com/github/copilot-cli/issues/3874)). If a deny does not hold in yours, tell us the version.

## Uninstall

`acp-uninstall` removes `~/.copilot/hooks/acp.json` along with everything else the installer wrote. Removing that one file by hand does the same for Copilot alone. The repo-level copy is a file in your repository; delete it there.

## Related integrations

- [Claude Code](/integrations/claude-code) — the hook contract Copilot reads
- [OpenAI Codex CLI](/integrations/codex) — the other half of most mixed fleets
- [Cursor](/integrations/cursor)
- [Set up permissions across your team's coding agents](/blog/set-up-permissions-across-your-teams-coding-agents) — one policy, every harness

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "HowTo",
  "name": "Install Agentic Control Plane for GitHub Copilot",
  "totalTime": "PT1M",
  "step": [
    {"@type": "HowToStep", "name": "Run the installer", "text": "curl -sf https://agenticcontrolplane.com/install.sh | bash . On Windows PowerShell: irm https://agenticcontrolplane.com/install.ps1 | iex"},
    {"@type": "HowToStep", "name": "Authenticate", "text": "Your browser opens to provision a workspace. The API key is saved to ~/.acp/credentials."},
    {"@type": "HowToStep", "name": "Restart Copilot", "text": "Restart Copilot CLI, or reload VS Code. Hook files are read at startup."},
    {"@type": "HowToStep", "name": "(Optional) Govern the whole team", "text": "Commit .github/hooks/acp.json to the repository so every contributor's Copilot CLI and VS Code agent mode run the hook."}
  ]
}
</script>
