Skip to content

GitHub Copilot CLI as a built-in harness, with live status through the Claude hooks - #215

Open
MusabaN wants to merge 1 commit into
AgentSystemLabs:mainfrom
MusabaN:copilot-cli-harness
Open

MusabaN wants to merge 1 commit into
AgentSystemLabs:mainfrom
MusabaN:copilot-cli-harness

Conversation

@MusabaN

@MusabaN MusabaN commented Nov 10, 2026 •

Copy link
Copy Markdown

Contents: ✨ What you get · 📸 Screenshots · 🧭 How it flows · ⚠️ Risk · 🔧 Technical overview · 📝 Notes

GitHub Copilot CLI joins the built-in harnesses: pick Copilot in the NEW SESSION PICKER and the session reports live status like a Claude one. Copilot CLI reads Claude's hook file and fires Claude-shaped payloads, so it rides the hooks nebula already installs; no new installer.

✨ What you get

🚀 Launch Copilot like any other harness

  • Copilot in the picker — n, then Copilot; also nebula spawn --kind copilot and quick_prompt_kind
    • launches copilot (npm i -g @github/copilot), verified on 1.0.108
    • a starting prompt is passed with -i and survives Copilot's folder-trust dialog
    • a harness whose CLI is missing is refused with the usual "not found" message
  • Model and effort — Settings › Agents › Copilot
    • passed as --model and --reasoning-effort
    • the model list belongs to the Copilot account, so the picker offers only default until harnesses.copilot.models lists ids
  • Restart resumes — copilot --resume <id>
    • the session id is captured from the first hook payload
    • Copilot reopens a session in the directory it recorded, so there is no relocation notice

🔔 Know when it needs you

  • Running, finished, RECENT PROMPTS — the same hooks as Claude
    • Copilot runs the hooks in <worktree>/.claude/settings.local.json
    • the card shows each submitted prompt
  • NEEDS FEEDBACK on a permission prompt or a question
    • no skip-permissions flag is passed, so Copilot keeps its own prompts; harnesses.copilot.permissions_flag: "--allow-all" opts out
    • its ask_user tool is reported as AskUserQuestion, so a question turns the row red too
    • an auto-approved tool call does not turn the row red (Copilot fires PermissionRequest for those as well; see the technical overview)
  • AUTO-TITLE — nebula rename on the first prompt
    • the instruction reaches Copilot in the reply shape it reads
    • the launch carries --allow-tool=shell(nebula:*) so the rename runs without a prompt
  • What stayed the same — Claude rows get byte-for-byte the replies and events they did before; the Copilot handling is keyed on the row's kind.

📸 Screenshots

The NEW SESSION PICKER after the change, captured with the SCREENSHOT HARNESS (make shot SCENE=copilot-picker KEYS="n"). Shown as terminal output because this PR comes from a fork, which cannot push to the pr-assets branch.

╭ New session ⌕ ╮
│ Claude      ▸ │
│ Codex       ▸ │
│ Cursor      ▸ │
│ Pi          ▸ │
│ Muse        ▸ │
│ Grok Build  ▸ │
│ OpenCode    ▸ │
│ Copilot     ▸ │
╰───────────────╯

A live run (real copilot 1.0.108 in a nebula pane, a prompt that runs a shell command, the permission dialog open):

$ nebula sessions --json
  "harness": "copilot",
  "name": "agent-1",
  "status": "needs_feedback",
  "summary": "please RUNTOOL now",

🧭 How it flows

sequenceDiagram
  participant C as copilot CLI
  participant H as HOOK RECEIVER (/api/hooks/claude)
  participant S as status machine
  Note over C: runs the hooks in .claude/settings.local.json
  C->>H: UserPromptSubmit {session_id, prompt}
  H-->>C: {"additionalContext": AUTO-TITLE instruction}
  H->>S: UserPromptSubmit → running
  C->>H: PermissionRequest (every tool call)
  Note over H: dropped for Copilot rows
  C->>H: Notification permission_prompt (dialog opened)
  H->>S: PermissionRequest → needs feedback
  C->>H: PostToolUse
  H->>S: running
  C->>H: Stop
  H->>S: finished
Loading

⚠️ Risk

🎯 Attack surface

  • The DAEMON execs a new program, copilot. Reached only by a user creating a Copilot session (or nebula spawn --kind copilot from a session they already run). Held by: the same registry, PATH probe and single-quoted login-shell argv as every other harness. Not held: what Copilot itself does once running, as for any harness.
  • A standing --allow-tool=shell(nebula:*) on every Copilot launch. It lets the agent run any nebula subcommand without a prompt, the same set Claude's Bash(nebula …:*) rules allow plus the rest. Held by: the pattern matches the nebula command only. Not held: it is broader than Claude's seven named subcommands; Copilot's per-subcommand matching is documented only for a fixed list of tools, so the narrower form was not verified. harnesses.copilot.launch_args overrides it.
  • No new route, socket, file or config source. Copilot posts to the existing Claude hook route with the existing bearer token.

Verdict: 🟢 Low risk — a new row in the harness table plus two kind-keyed branches in the hook receiver; nothing changes for a session that is not Copilot.

Level Why
🔒 Security & production Low one new exec'd CLI behind the existing harness path, and a launch-time allow rule scoped to the nebula command
⚡ Performance Low one extra get_agent store read per hook POST on the Claude route; off the TUI draw and PTY byte paths
🧩 Fit with the codebase Low a descriptor row and exhaustive-match arms, as Grok and OpenCode were added; the hook dialect is reused rather than a sixth one invented

Rollback: git revert removes the harness. It does not undo the PROTOCOL VERSION bump on already-running daemons, and Copilot rows already in a store would then load as Claude rows (parse_agent_kind falls back to the default kind).

🔧 Technical overview

  • Mechanism. AgentKind::Copilot gets a HarnessDescriptor with hooks: "claude", so spawn installs the Claude hooks and Copilot posts to /api/hooks/claude. The receiver looks up the row's kind and, for Copilot, (a) answers UserPromptSubmit with a top-level additionalContext, because Copilot parses and drops Claude's hookSpecificOutput envelope, and (b) drops PermissionRequest and maps the permission_prompt notification to it, because Copilot fires PermissionRequest for auto-approved calls and its notification is immediate, not deferred like Claude's.
  • Files. crates/nebula-core/src/harness.rs — the descriptor row, with the verified flags in its comment; crates/nebula-core/src/entities.rs — the kind; crates/nebula-daemon/src/hooks/mod.rs — copilot_prompt_reply, copilot_event and their tests; crates/nebula-daemon/src/registry.rs — the spawn-argv test; crates/nebula-tui/src/agent_picker.rs — the label; docs under docs/.
  • Not done. A separate copilot hook dialect installing into ~/.copilot: it would avoid the per-worktree trust dialog gating hooks, but Copilot would still also run the Claude hooks left in a shared worktree and double-report. No model catalogue: the list is per account. No system-prompt guidance: Copilot has no flag for it.
  • Gate. cargo fmt --check, cargo clippy --workspace --all-targets --locked -- -D warnings and cargo test --workspace green. End to end against copilot 1.0.108 driven by a local mock model through its bring-your-own-provider mode: create from the TUI, permission wait, approve, daemon restart and resume. The issue's author has since run it with a signed-in account and a real model, where Copilot issued AUTO-TITLE's nebula rename. Not exercised: Copilot subagent (task tool) hook traffic.

📝 Notes

To test locally:

  1. Install and sign in to Copilot CLI (npm i -g @github/copilot, then copilot and /login).
  2. Build this branch and run it in a fresh isolated instance (make dev SEED=0), then add a test checkout. The nebula on PATH must speak protocol 49 or later, or the session's nebula rename is refused with a protocol mismatch.
  3. Press n, pick Copilot, and give it a prompt that runs a command, such as "create a file called hello.txt". Answer Copilot's folder-trust dialog; the prompt runs once you do.
  4. Expect the row to turn red while Copilot asks to run the command and return to running when you approve, the card to show the prompt, and agent-N to become a short title.
  5. Stop the dev daemon, start it again and open the session. Expect the conversation to be back, launched as copilot --allow-tool=shell(nebula:*) --resume <id>.

Prepared using the repository's PR description skill, benefit-groups template.

🤖 Generated with Claude Code

…e Claude hooks

Copilot CLI (verified on 1.0.108) reads hooks from Claude's settings
file and fires them with Claude's event names and payloads, so it rides
the Claude hook dialect: status, recent prompts, session id capture,
permission prompts and ask_user questions all report without a new
installer. Three things are Copilot's own:

- The first prompt rides -i (no positional prompt) and a restart runs
  copilot --resume <id>.
- Injected context is read only from a top-level additionalContext, so
  the auto-title instruction goes out in that shape for Copilot rows.
- PermissionRequest fires for every tool call, including auto-approved
  ones, so it is dropped and the permission_prompt notification, which
  fires only as a dialog opens, is the wait.

The launch carries --allow-tool=shell(nebula:*) so the auto-title
nebula rename runs unprompted; Copilot reads no permission rules from
Claude's settings. PROTOCOL_VERSION 51 -> 52 for the new AgentKind;
additive, so MIN_COMPATIBLE_PROTOCOL stays at 49.

Closes AgentSystemLabs#211

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3h4btM4mhdF58HbnSsXnC
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: Support for Copilot CLI

2 participants