Copilot Parity Control Plane Reference¶
Overview¶
The Copilot parity control plane gives GitHub Copilot CLI the same staged amplihack surfaces that Claude Code receives through .claude/ settings, while respecting Copilot's native .github/ hook and agent discovery model.
Components¶
| Component | Path | Responsibility |
|---|---|---|
| Copilot launcher | src/amplihack/launcher/copilot.py | Stages agents, hooks, commands, recipes, and generated wrappers before launching Copilot CLI |
| Rust recipe runner bridge | src/amplihack/recipes/rust_runner.py | Discovers recipe-runner-rs, enforces version compatibility, builds subprocess environment, and parses JSON results |
| Nested Copilot compatibility layer | src/amplihack/recipes/rust_runner_copilot.py | Merges prompt fragments and injects permissive defaults only when explicit Copilot permission flags are absent |
| Canonical XPIA hook | .claude/tools/xpia/hooks/pre_tool_use.py | Fail-closed Bash policy evaluation backed by xpia-defend |
| XPIA compatibility shim | .claude/tools/xpia/hooks/pre_tool_use_rust.py | Delegates to pre_tool_use.py so both historical entrypoints behave identically |
| Generated wrapper | .github/hooks/pre-tool-use | Emits the single Copilot-facing permission payload after evaluating amplihack and XPIA outputs |
Generated Copilot Hook Wrappers¶
| Wrapper | Generated scripts | Notes |
|---|---|---|
.github/hooks/session-start | session_start.py | Single-script wrapper |
.github/hooks/session-stop | stop.py, session_stop.py | Multi-script wrapper; captures stdin once and pipes it to both hooks |
.github/hooks/pre-tool-use | pre_tool_use.py plus XPIA pre_tool_use.py | Special wrapper with JSON aggregation |
.github/hooks/post-tool-use | post_tool_use.py | Single-script wrapper |
.github/hooks/user-prompt-submit | user_prompt_submit.py, workflow_classification_reminder.py | Multi-script wrapper |
Pre-Tool-Use Decision Precedence¶
The generated .github/hooks/pre-tool-use wrapper evaluates both hook stacks and emits one final JSON object.
| Priority | Source | Accepted signal | Result |
|---|---|---|---|
| 1 | XPIA | permissionDecision = allow, deny, or ask | Return the XPIA payload unchanged |
| 2 | amplihack | permissionDecision = allow, deny, or ask | Return the amplihack payload unchanged |
| 3 | amplihack | block: true | Convert to {"permissionDecision":"deny","message":...} |
| 4 | none | no explicit decision | Return {} |
This contract keeps XPIA in control of explicit Bash security decisions while preserving existing amplihack block semantics.
XPIA Hook Contract¶
Input¶
pre_tool_use.py accepts JSON on stdin or as the first argv value.
{
"tool_name": "Bash",
"tool_input": {
"command": "pwd"
},
"cwd": "/path/to/repo",
"session_id": "optional-session-id"
}
Output¶
Allow:
Deny:
Canonical and compatibility entrypoints¶
| Entrypoint | Behavior |
|---|---|
.claude/tools/xpia/hooks/pre_tool_use.py | Canonical fail-closed hook |
.claude/tools/xpia/hooks/pre_tool_use_rust.py | Delegates to the canonical hook |
Audit logging¶
The canonical XPIA hook writes audit events to:
Rust Runner Discovery and Execution¶
Binary discovery order¶
rust_runner.py resolves the runner in this order:
RECIPE_RUNNER_RS_PATHrecipe-runner-rsonPATH~/.cargo/bin/recipe-runner-rs~/.local/bin/recipe-runner-rs
If the runner is still missing, the bridge raises RustRunnerNotFoundError.
Version gating¶
The bridge checks the discovered binary before execution. Unknown, unparseable, or too-old versions are rejected with an explicit version error. The Rust-selected path does not silently fall back to a Python runner.
Startup banners¶
The bridge emits two stderr banners during execution:
Response contract¶
The Rust runner must emit JSON on stdout. If stdout is unparseable:
- non-zero exit codes become explicit runtime errors
- signal termination is surfaced as a signal-specific error
- empty or malformed stdout becomes an "unparseable output" error
Nested Copilot Normalization Rules¶
The compatibility layer normalizes nested Copilot launches created by the Rust recipe runner.
Prompt merging¶
The normalizer removes and merges these flags into one final -p payload:
--system-prompt--append-system-prompt-p--prompt=
Merged prompt parts are joined with a blank line.
Permission preservation¶
The normalizer treats these as explicit tool-permission flags:
--allow-all-tools--allow-tool--deny-tool
It treats these as explicit path-permission flags:
--allow-all-paths--allow-path--deny-path
If no explicit tool or path permission appears, it prefixes the nested command with:
If explicit flags are already present, it preserves them and does not widen permissions.
Environment Variables¶
| Variable | Scope | Default | Meaning |
|---|---|---|---|
AMPLIHACK_AGENT_BINARY | launcher and nested runner | claude | Selects the agent binary for nested recipe execution |
AMPLIHACK_HOOK_ENGINE | launcher | auto-detect | Selects rust or python for amplihack hook staging |
RECIPE_RUNNER_RS_PATH | Rust runner bridge | unset | Explicit path to recipe-runner-rs |
RECIPE_RUNNER_INSTALL_TIMEOUT | Rust runner install helper | 300 | Timeout, in seconds, for auto-install attempts |
Context Spillover Rules¶
Large recipe context values are passed safely.
| Limit | Behavior |
|---|---|
< 32,768 UTF-8 bytes | Passed inline via --set key=value |
>= 32,768 UTF-8 bytes | Spilled to a temp file and passed as a file:// URI |
Spill directories are created with tempfile.mkdtemp(...), which produces a private process-scoped directory and avoids predictable temp paths.
Safe Command Example¶
printf '%s\n' '{"tool_name":"Bash","tool_input":{"command":"pwd"}}' \
| python3 .claude/tools/xpia/hooks/pre_tool_use.py
# Output: {}