feat: ArmorCopilot for GitHub Copilot CLI — initial port from ArmorCodex#3
Merged
Merged
Conversation
ArmorCopilot is the GitHub Copilot CLI counterpart of ArmorClaude
(Claude Code) and ArmorCodex (OpenAI Codex). Same wedge — intent
capture before action, policy enforcement on every tool call, audit to
ArmorIQ backend — applied to the local-CLI agentic harness that has
real preToolUse/postToolUse hooks. Pivoted to GitHub Copilot CLI after
Microsoft Copilot Studio research showed it lacks pre-action intent
visibility (only post-LLM-decision webhook), which would degrade us
to a policy-only product comparable to Check Point + Zenity. GitHub
Copilot CLI is local + hooks + MCP, identical model to Claude/Codex.
Phase 0 PoC questions answered from docs (no code experiment needed):
- Plugin manifest at .claude-plugin/plugin.json (canonical)
- Hook payload via stdin JSON (snake_case + camelCase variants)
- Block via stdout JSON {permissionDecision:"deny",permissionDecisionReason}
- MCP config at .mcp.json or .github/mcp.json
- 13+ hook events available (sessionStart/Pre/Post/userPromptSubmitted/etc)
- Plugin install via `copilot plugin install owner/repo`
- 2 default marketplaces: github/copilot-plugins + github/awesome-copilot
Phase 1 port — 22 files, ~85% verbatim from armorCodex/plugins/armorcodex:
packages/armorcopilot-gh/
├── .claude-plugin/plugin.json plugin manifest (canonical path)
├── .mcp.json MCP server config
├── hooks/hooks.json 5 hooks: sessionStart, userPromptSubmitted,
│ preToolUse, permissionRequest, postToolUse
├── package.json npm deps + name @armoriq/armorcopilot-gh
├── README.md install + config + architecture
├── assets/armoriq-logo.png
└── scripts/
├── bootstrap.mjs entry point (mcp / router)
├── hook-router.mjs dispatches hook events to engine
├── policy-mcp.mjs MCP server (3 tools)
└── lib/ 12 modules
├── engine.mjs ZERO-DIFF from armorCodex
├── hook-output.mjs ZERO-DIFF (snake_case + permissionDecision
│ shapes already match GH Copilot's spec)
├── audit-wal.mjs VERBATIM
├── iap-service.mjs VERBATIM
├── policy.mjs VERBATIM + tool whitelist expanded for GH
├── crypto-policy.mjs VERBATIM
├── fs-store.mjs VERBATIM
├── runtime-state.mjs VERBATIM
├── common.mjs VERBATIM
├── intent-schema.mjs VERBATIM
├── intent.mjs VERBATIM
└── config.mjs env prefix ARMORCODEX_ → ARMORCOPILOT_,
data dir ~/.codex/armorcodex →
~/.copilot/armorcopilot, llmId
openai-codex → github-copilot,
mcpName/agentId/userId rebranded
All Codex identifier references swept from code AND comments. Plugin
runtime auto-discovers .claude-plugin/plugin.json on
`copilot plugin install armoriq/armorCopilot:packages/armorcopilot-gh`.
Refs #1, parks #2 (the previous Microsoft Copilot Studio skeleton —
preserved as tag stash/armorcopilot-ms-2026-05-22 for future revival).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Ports ArmorCopilot’s intent-plan + policy enforcement model into a GitHub Copilot CLI plugin, wiring Copilot hooks to a local enforcement engine and exposing an MCP server for plan registration and policy management.
Changes:
- Added Copilot CLI plugin manifests (hooks + MCP server) and a bootstrap entrypoint for dependency setup.
- Implemented hook routing + enforcement engine (intent-plan capture/verification, policy evaluation, audit logging).
- Added local persistence primitives (policy state, runtime state, audit WAL) and backend integrations (intent capture + verify-step + audit batch ship).
Reviewed changes
Copilot reviewed 21 out of 22 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/armorcopilot-gh/scripts/policy-mcp.mjs | MCP server exposing register_intent_plan, policy_read, policy_update, plus audit WAL flusher |
| packages/armorcopilot-gh/scripts/lib/runtime-state.mjs | Runtime state persistence (sessions + discovered tools) |
| packages/armorcopilot-gh/scripts/lib/policy.mjs | Policy model, parsing (“Policy …” commands), and evaluation (incl. anyParam matchers + data class detection) |
| packages/armorcopilot-gh/scripts/lib/planner.mjs | Plan markdown/JSON extraction helpers and plan file path resolution |
| packages/armorcopilot-gh/scripts/lib/intent.mjs | Intent capture + token/plan parsing + plan/tool matching + CSRG proof extraction |
| packages/armorcopilot-gh/scripts/lib/intent-schema.mjs | Zod schema + normalization for Copilot intent plans |
| packages/armorcopilot-gh/scripts/lib/iap-service.mjs | Backend calls for verify-step / CSRG verify / audit enqueue & batch ship |
| packages/armorcopilot-gh/scripts/lib/hook-output.mjs | Hook response helpers for deny/block/additional context |
| packages/armorcopilot-gh/scripts/lib/fs-store.mjs | Atomic JSON read/write utilities |
| packages/armorcopilot-gh/scripts/lib/engine.mjs | Core enforcement pipeline for hooks (policy + intent + CSRG + audit) |
| packages/armorcopilot-gh/scripts/lib/crypto-policy.mjs | Optional crypto-bound policy token flow and local cached state |
| packages/armorcopilot-gh/scripts/lib/config.mjs | Plugin/env configuration resolution |
| packages/armorcopilot-gh/scripts/lib/common.mjs | Shared utilities (matching, sanitization, redaction, HTTP helpers) |
| packages/armorcopilot-gh/scripts/lib/audit-wal.mjs | Durable on-disk audit WAL with offset tracking + rotation |
| packages/armorcopilot-gh/scripts/hook-router.mjs | Routes hook events from stdin JSON to engine handlers |
| packages/armorcopilot-gh/scripts/bootstrap.mjs | One-time dependency install + stdio safety (stdout hygiene) + dispatch |
| packages/armorcopilot-gh/README.md | User-facing install/config/architecture docs |
| packages/armorcopilot-gh/package.json | Package metadata and runtime dependencies |
| packages/armorcopilot-gh/hooks/hooks.json | Copilot hook registration to the router |
| packages/armorcopilot-gh/.mcp.json | MCP server registration (stdio node command) |
| packages/armorcopilot-gh/.claude-plugin/plugin.json | Plugin manifest (hooks, MCP servers, and userConfig surface) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+42
to
+50
| // dev branch — points at staging-api for pre-release testing. | ||
| // main branch keeps "https://api.armoriq.ai" (prod). When promoting a | ||
| // feature from dev → main, resolve the URL conflict in favor of main. | ||
| const backendEndpoint = | ||
| env.ARMORCOPILOT_BACKEND_ENDPOINT?.trim() || | ||
| env.BACKEND_ENDPOINT?.trim() || | ||
| (useProduction | ||
| ? "https://staging-api.armoriq.ai" | ||
| : "http://127.0.0.1:3000"); |
Comment on lines
+81
to
+88
| const response = await postJson(endpoint, payload, headers, timeoutMs); | ||
| if (!response.ok && !isPlainObject(response.data)) { | ||
| throw new Error( | ||
| response.text || `IAP verify-step failed with status ${response.status}` | ||
| ); | ||
| } | ||
|
|
||
| const data = isPlainObject(response.data) ? response.data : {}; |
Comment on lines
+221
to
+247
| const pendingPath = path.join(config.dataDir, "pending-plan.json"); | ||
| await writeJson(pendingPath, { | ||
| plan, | ||
| tokenRaw: "", | ||
| allowedActions: Array.from(extractAllowedActions(plan)), | ||
| expiresAt: undefined, | ||
| registeredAt: Date.now() | ||
| }); | ||
|
|
||
| let backendWillIssue = false; | ||
| if (config.intentEndpoint || (config.useSdkIntent && config.apiKey)) { | ||
| backendWillIssue = true; | ||
| // Kick off the SDK call. When it resolves with a signed token, update | ||
| // pending-plan.json so PreToolUse picks up the token on subsequent | ||
| // calls. Errors are logged to stderr and otherwise swallowed. | ||
| (async () => { | ||
| try { | ||
| const policyState = await loadPolicyState(config.policyFile); | ||
| const result = await requestIntent(config, { | ||
| prompt: parsed.data.goal, | ||
| plan, | ||
| session_id: "mcp", | ||
| policy_hash: computePolicyHash(policyState.policy), | ||
| policy: policyState.policy, | ||
| validitySeconds: config.validitySeconds, | ||
| metadata: { source: "copilot", planning: "copilot-registered" } | ||
| }); |
Comment on lines
+120
to
+124
| const st = await fh.stat(); | ||
| if (offset >= st.size) return { rows: [], endOffset: offset }; | ||
| const length = st.size - offset; | ||
| const buf = Buffer.alloc(length); | ||
| await fh.read(buf, 0, length, offset); |
Comment on lines
+14
to
+20
| ## Install | ||
|
|
||
| ```bash | ||
| copilot plugin install armoriq/armorCopilot | ||
| ``` | ||
|
|
||
| The plugin runtime auto-discovers `.claude-plugin/plugin.json` and registers hooks + MCP servers. |
Comment on lines
+11
to
+12
| - Optional CSRG cryptographic proofs for tamper detection | ||
| - Synchronous audit log to ArmorIQ backend |
Comment on lines
+18
to
+38
| "preToolUse": [ | ||
| { | ||
| "type": "command", | ||
| "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bootstrap.mjs\" router", | ||
| "timeoutSec": 30 | ||
| } | ||
| ], | ||
| "permissionRequest": [ | ||
| { | ||
| "type": "command", | ||
| "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bootstrap.mjs\" router", | ||
| "timeoutSec": 30 | ||
| } | ||
| ], | ||
| "postToolUse": [ | ||
| { | ||
| "type": "command", | ||
| "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bootstrap.mjs\" router", | ||
| "timeoutSec": 30 | ||
| } | ||
| ] |
Mirrors the established ArmorIQ plugin repo layout: / ├── .claude-plugin/marketplace.json repo-level marketplace ├── .agents/plugins/marketplace.json mirror ├── plugins/<plugin-name>/ actual plugin tree └── README.md Changes: - Renamed packages/armorcopilot-gh/ → plugins/armorcopilot/ (drop the "-gh" suffix; the brand is just ArmorCopilot, GitHub Copilot CLI is the only target. If we ever ship for another harness, that gets its own plugin directory under plugins/.) - Added .claude-plugin/marketplace.json at repo root with publisher "ArmorIQ" (matches armorCodex#22 convention — the "Built by..." filter groups plugins by company, not product) - Mirror to .agents/plugins/marketplace.json - package.json name: @armoriq/armorcopilot-gh → @armoriq/armorcopilot - README.md rewritten for the new layout - Removed PLAN_GH_COPILOT.md + CTO_MEETING_BRIEF.md (planning docs for this work — preserved in this branch's commit history if needed; no value at repo root after the port lands) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After installing into the real `copilot` CLI and capturing what it sends
to hooks, two adapter changes were needed:
1. hooks.json — pass COPILOT_HOOK_EVENT via env per event
Copilot CLI does NOT include `hook_event_name` in the hook payload
(unlike Claude Code's snake_case variant). Our engine routes by
event name, so we set it via the `env` field per hook.
2. hook-router.mjs — normalize Copilot's camelCase payload
Copilot sends `sessionId / toolName / toolArgs (string-encoded)`
where the engine expects `session_id / tool_name / tool_input (obj)`.
Added a normalizer that copies camelCase → snake_case, parses
`toolArgs` JSON string to object, and sets `hook_event_name` from
the env var.
3. marketplace.json — fix `source` shape
Copilot's marketplace schema rejected `{source:"local",path:"./..."}`.
Changed to plain string `"./plugins/armorcopilot"` matching the
format the official github/copilot-plugins marketplace uses.
4. README.md — fixed leftover `packages/armorcopilot-gh` path
Verified end-to-end with `copilot plugin marketplace add` + install:
Marketplace "armorcopilot" added successfully.
Plugin "armorcopilot" installed successfully.
[DEBUG] Loaded 5 hook(s) from 1 plugin(s)
[DEBUG] Adding tool: armorcopilot-policy-policy_update
[DEBUG] Adding tool: armorcopilot-policy-policy_read
[DEBUG] Adding tool: armorcopilot-policy-register_intent_plan
[hook stderr] [armorcopilot] session started: ..., mode=enforce
[hook stdout] {"hookSpecificOutput":{"hookEventName":"UserPromptSubmit",
"additionalContext":"ArmorCopilot active. Call
register_intent_plan first..."}}
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment on lines
+45
to
+50
| const backendEndpoint = | ||
| env.ARMORCOPILOT_BACKEND_ENDPOINT?.trim() || | ||
| env.BACKEND_ENDPOINT?.trim() || | ||
| (useProduction | ||
| ? "https://staging-api.armoriq.ai" | ||
| : "http://127.0.0.1:3000"); |
Comment on lines
+264
to
+274
| const safeInternalTools = new Set([ | ||
| "toolsearch", | ||
| "todowrite", | ||
| "listmcpresourcestool", | ||
| "readmcpresourcetool", | ||
| "read", | ||
| "grep", | ||
| "glob", | ||
| "websearch", | ||
| "webfetch" | ||
| ]); |
| // SDK round-trip, even cold-start latency) eats into that budget. | ||
| // Awaiting nothing on the network path keeps the MCP response under | ||
| // ~100ms regardless of backend conditions. | ||
| const pendingPath = path.join(config.dataDir, "pending-plan.json"); |
| - Verifies every tool call against the registered plan — out-of-plan tools are blocked even if policy would allow them | ||
| - Lets you set policies in natural language ("Block any commands that fetch URLs") via the `policy_update` MCP tool | ||
| - Optional CSRG cryptographic proofs for tamper detection | ||
| - Synchronous audit log to ArmorIQ backend |
| ## Install | ||
|
|
||
| ```bash | ||
| copilot plugin install armoriq/armorCopilot |
| ## Install | ||
|
|
||
| ```bash | ||
| copilot plugin install armoriq/armorCopilot |
The relative `bash: "node scripts/bootstrap.mjs router"` only worked when Copilot CLI happened to chdir to the plugin root before invoking the hook. From other working directories the hook crashed with: Error: Cannot find module '/<user-cwd>/scripts/bootstrap.mjs' Switched all 5 hook commands to expand \$CLAUDE_PLUGIN_ROOT (set by Copilot CLI for every plugin hook). Now the bash command is: node "\$CLAUDE_PLUGIN_ROOT/scripts/bootstrap.mjs" router This is the same pattern Claude Code uses and is what Copilot CLI expects for plugins shipping scripts under their own plugin root. Verified end-to-end: running `copilot` from /Users/<user>/Armoriq (any non-plugin CWD) no longer hits the MODULE_NOT_FOUND error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-user flow: curl -fsSL https://armoriq.ai/install_armorcopilot.sh | bash What it does (in order): 1. Checks prereqs: copilot CLI, node>=20, npm, git 2. Clones armoriq/armorCopilot to ~/.armoriq/armorCopilot 3. npm install --omit=dev for plugin runtime deps 4. npm install -g @armoriq/sdk-dev@latest (installs `armoriq-dev` CLI) 5. copilot plugin marketplace add armoriq/armorCopilot 6. copilot plugin install armorcopilot@armorcopilot 7. armoriq-dev login --product armorcopilot (device-code auth) Idempotent — re-running pulls latest, refreshes deps, reinstalls plugin. Flags: --uninstall remove plugin + marketplace registration --skip-login don't prompt for ArmorIQ login at the end Overrides (testing): ARMORCOPILOT_MARKETPLACE_REPO override marketplace source ARMORCOPILOT_GIT_URL override clone source ARMORCOPILOT_GIT_REF branch / tag (default main) ARMORCOPILOT_INSTALL_HOME where to clone Also commits the plugin's package-lock.json (generated by the local npm install during PoC testing) for reproducible installs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…art) The ASCII art block in install_armorcopilot.sh was copy-pasted from install_armorcodex.sh and never updated, so the banner that flashed during install said ARMORCODEX instead of ARMORCOPILOT. Replaced with ANSI Shadow font output for the correct 'ARMORCOPILOT' wordmark. Verified by running the installer locally — banner now reads correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six review-driven fixes, in priority order: 1. config.mjs — endpoint resolution is now a 3-way switch on envMode (production/staging/local) instead of a confusing 2-way ternary that mapped useProduction → staging-api. Production users now correctly resolve to api.armoriq.ai unless overridden. 2. iap-service.mjs verify-step — fail closed on non-2xx. Previously a non-2xx response with a JSON body skipped the throw and fell through to defaulting `allowed=true`. Now any non-ok response throws. 3. engine.mjs safe-tools whitelist — dropped `websearch` and `webfetch` from the early-exit list. Network egress tools must go through full policy + intent enforcement; they were previously bypassing all guardrails. 4. audit-wal.mjs readBatch — capped per-read size at 4MiB to prevent OOM if the backend is down and the WAL grows large. Plenty of room for `maxRows=100` audit rows (40KB worst case each). 5. policy-mcp.mjs register_intent_plan + intent-schema.mjs — added optional `session_id` field to the plan. When the caller passes it, the pending plan is written to `pending-plan.<sessionId>.json` (per-session, matches what engine.mjs already reads). We also mirror to the legacy global path so older readers still work. Eliminates concurrent-session clobbering. 6. hooks/hooks.json — added the 3 missing registrations (postToolUseFailure, agentStop/Stop, sessionEnd). Engine had handlers for all of these but Copilot CLI never invoked them because they weren't registered. Failed tool executions and session cleanup now route through ArmorCopilot. 7. README.md — corrected the "synchronous audit log" claim to reflect the actual WAL-backed async batched pipeline, and updated the install command to show the marketplace add + install flow (plus the curl-pipe installer alternative). Refs Copilot review on PR #3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment on lines
+7
to
+13
| ## Install | ||
|
|
||
| ```bash | ||
| copilot plugin install armoriq/armorCopilot | ||
| ``` | ||
|
|
||
| The plugin runtime auto-discovers `.claude-plugin/plugin.json` inside `plugins/armorcopilot/` and registers hooks + MCP servers. |
Comment on lines
+187
to
+197
| inputSchema: { | ||
| goal: z.string().min(1).optional() | ||
| .describe("One-line summary of what the plan accomplishes"), | ||
| steps: z.union([ | ||
| z.array(PLAN_STEP_SCHEMA).min(1), | ||
| z.string().min(1) | ||
| ]).optional() | ||
| .describe("Ordered list of tool calls (array, or JSON-stringified array)"), | ||
| plan: z.union([INTENT_PLAN_ZOD, z.string().min(1)]).optional() | ||
| .describe("Alternative: pass the whole plan as an object or JSON string") | ||
| } |
Comment on lines
+258
to
+266
| const result = await requestIntent(config, { | ||
| prompt: parsed.data.goal, | ||
| plan, | ||
| session_id: "mcp", | ||
| policy_hash: computePolicyHash(policyState.policy), | ||
| policy: policyState.policy, | ||
| validitySeconds: config.validitySeconds, | ||
| metadata: { source: "copilot", planning: "copilot-registered" } | ||
| }); |
| "interface": { | ||
| "displayName": "ArmorCopilot", | ||
| "shortDescription": "Intent-based security policy and audit for GitHub Copilot CLI.", | ||
| "longDescription": "ArmorIQ intent-based security enforcement for GitHub Copilot CLI. Hooks into preToolUse / postToolUse / sessionStart / userPromptSubmitted events. Provides plan registration through MCP, intent-plan matching, permission gating, and synchronous audit on every tool invocation. Block tools by name, by argument pattern, or by intent drift — all configured from natural language via the policy_update MCP tool.", |
Comment on lines
+1
to
+10
| { | ||
| "name": "@armoriq/armorcopilot", | ||
| "version": "0.1.0", | ||
| "lockfileVersion": 3, | ||
| "requires": true, | ||
| "packages": { | ||
| "": { | ||
| "name": "@armoriq/armorcopilot", | ||
| "version": "0.1.0", | ||
| "dependencies": { |
Comment on lines
+4
to
+29
| # ArmorCopilot installer for GitHub Copilot CLI. | ||
| # | ||
| # Usage: | ||
| # curl -fsSL https://armoriq.ai/install_armorcopilot.sh | bash | ||
| # | ||
| # Works two ways: | ||
| # A. curl-pipe (no clone): fetches the plugin into ~/.armoriq/armorCopilot | ||
| # B. From an existing checkout: cd armorCopilot && bash install_armorcopilot.sh | ||
| # | ||
| # What it wires: | ||
| # 1. clones the plugin to ~/.armoriq/armorCopilot | ||
| # 2. npm install --omit=dev for plugin runtime deps | ||
| # 3. installs @armoriq/sdk-dev globally (for the `armoriq-dev` CLI) | ||
| # 4. registers the marketplace + installs the plugin in Copilot CLI: | ||
| # copilot plugin marketplace add armoriq/armorCopilot | ||
| # copilot plugin install armorcopilot@armorcopilot | ||
| # 5. runs `armoriq-dev login --product armorcopilot` for device-code auth | ||
| # | ||
| # Idempotent: re-running pulls the latest, reinstalls deps, refreshes marketplace. | ||
| # | ||
| # Flags: | ||
| # --uninstall remove the plugin + marketplace registration | ||
| # --skip-login don't prompt for ArmorIQ login at the end | ||
| # | ||
| # Non-interactive overrides: | ||
| # ARMORCOPILOT_MARKETPLACE_REPO override marketplace source (testing) |
Pulkit7070
approved these changes
May 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
ArmorCopilot for GitHub Copilot CLI, ported from ArmorCodex with ~85% verbatim code reuse. Replaces the previously-explored Microsoft Copilot Studio direction (which couldn't carry our intent-capture wedge).
Why GitHub Copilot CLI (not Microsoft Copilot Studio)
preToolUsehook BEFORE tool firesPer the CTO meeting brief and product principle "intent is the main product — if not, don't do it" — we pivot.
The previous MS work is preserved as tag
stash/armorcopilot-ms-2026-05-22for future revival if a customer asks.What landed in this PR
Final layout matches
armorCodex/armorClaude(plugin atplugins/<name>/with repo-level.claude-plugin/marketplace.jsonat root):Code reuse from armorCodex (~85% verbatim)
engine.mjs,hook-output.mjs,audit-wal.mjs,iap-service.mjs,policy.mjs,crypto-policy.mjs,fs-store.mjs,runtime-state.mjs,common.mjs,intent-schema.mjs,intent.mjs,policy-mcp.mjs— copied verbatim or with comment-only renamesconfig.mjs— env prefixARMORCODEX_→ARMORCOPILOT_, data dir~/.codex/armorcodex→~/.copilot/armorcopilot, llmIdopenai-codex→github-copilot, mcpName/agentId/userId rebrandedhook-router.mjs— added normalizer that maps Copilot's camelCase payload (sessionId,toolName,toolArgsJSON-string) to the snake_case shape engine.mjs already expectsPhase 0 research findings
.claude-plugin/plugin.json(4 alternate paths checked; this is canonical and matchesgithub/copilot-plugins){"permissionDecision":"deny","permissionDecisionReason":"...","modifiedArgs":{}}— matches our existinghook-output.mjsZERO-DIFF$CLAUDE_PLUGIN_ROOT(we use this inhooks.jsonfor absolute-path resolution)copilot plugin install owner/repoorowner/repo:pathgithub/copilot-plugins(official) +github/awesome-copilot(community)How to install (once merged)
Via the marketplace flow (the repo's root
.claude-plugin/marketplace.jsonresolves the plugin source automatically):Or via the curl-pipe installer (handles plugin install + npm deps +
armoriq-devCLI + device-code login):curl -fsSL https://armoriq.ai/install_armorcopilot.sh | bashThe plugin runtime auto-discovers
.claude-plugin/plugin.jsonatplugins/armorcopilot/and registers hooks + MCP servers.Companion PRs (must all merge for the curl-pipe flow to work end-to-end)
'armorcopilot'to ProductSlug allowlist (backend)install_armorcopilot.shfrom armoriq.aiLocal install + smoke test (verified)
The plugin was installed end-to-end via local marketplace path and tested against a real
copilotCLI session:register_intent_plan,policy_update,policy_readtoolsCopilot review fixes (latest commit
59da8a2)Six review-driven fixes addressed:
config.mjs— 3-way switch on envMode (production/staging/local) replacing the confusing 2-way ternary that mapped useProduction → stagingiap-service.mjsverify-step — fail closed on non-2xx (was silently allowing through withdata.alloweddefault)engine.mjssafe-tools — droppedwebsearch+webfetchfrom the early-exit list (network egress must go through enforcement)audit-wal.mjs— capped readBatch at 4MiB to prevent OOM if the backend is down and the WAL grows largepolicy-mcp.mjs+intent-schema.mjs— added optionalsession_idto plan; MCP writes per-sessionpending-plan.<sessionId>.json(with global mirror for legacy reader compat). Eliminates concurrent-session clobbering.hooks.json— added the 3 missing event registrations (postToolUseFailure,agentStop,sessionEnd) — engine had handlers but Copilot never invoked themREADME.md— corrected "synchronous audit log" to reflect the actual WAL-backed async batched pipeline; updated install command to show marketplace add + curl-pipe flowTest plan
copilot plugin marketplace add /Users/...armorCopilot+copilot plugin install armorcopilot@armorcopilotLoaded 5 hook(s) from 1 plugin(s)→ now 8)armorcopilot-policy-policy_update,_read,register_intent_plan)Closes #1, parks #2 (the previous Microsoft Copilot Studio skeleton — preserved as tag
stash/armorcopilot-ms-2026-05-22for future revival).🤖 Generated with Claude Code