Skip to content

feat: GitHub Copilot CLI support for the armoctl plugin - #39

Merged
slashben merged 5 commits into
mainfrom
feat/copilot-cli-plugin
Oct 25, 2026
Merged

slashben merged 5 commits into
mainfrom
feat/copilot-cli-plugin

Conversation

@slashben

@slashben slashben commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Source of Truth: SUB-9439

Summary

copilot plugin marketplace add armosec/armoctl + copilot plugin install armoctl@armosec now gives Copilot CLI the same behaviour as Claude Code. No Copilot-specific manifest was needed: Copilot reads the existing .claude-plugin/ files, skills/, and hooks/hooks.json.

Contrary to the starting assumption, Copilot does expand the existing hook command. It exports CLAUDE_PLUGIN_ROOT into the hook environment, so ${CLAUDE_PLUGIN_ROOT}${extensionPath}/hooks/session-start.sh already resolved. Testing turned up other problems, mostly not Copilot-specific, and this PR fixes them:

  1. Hook messages were invisible in Copilot. Copilot reads only the top-level additionalContext; the hook emitted only hookSpecificOutput.additionalContext (Claude/Gemini). It now emits both.
  2. The hook ran armoctl update on every session, in every host. Released binaries print armoctl version unknown (built from source) for --version (fang falls back to Go build info because fang.WithVersion was never passed). The hook parsed the last word, source), which never matches. The hook now reads armoctl version (which writes to stderr), and main.go passes fang.WithVersion(Version).
  3. The hook overwrote local builds. During testing it replaced a from-source ~/.local/bin/armoctl with the release. It now skips auto-update for non-release versions (dev, v0.0.13-dirty, v0.0.12-4-gabc123).
  4. Install output broke the hook JSON. install.sh prints progress to stdout, which hosts parse as hook JSON, so the "just installed" message was dropped. Installer output now goes to stderr.
  5. Failed downloads looked like successes. In curl … | bash without pipefail, a failed curl pipes nothing into bash, which exits 0. Fixed.
  6. Agents ran --yes without asking (all hosts). The root skill said to re-run with --yes "once you're confident", and agents usually load only the cluster skill, which had no mutation guidance. Copilot (claude-sonnet-5.5) and Claude Code (Sonnet 5.5) both dry-ran and then went straight to --yes. The root skill now requires showing the dry-run, waiting for an explicit yes, and re-running the identical command. gen-skill-docs adds the same rule as a ## Mutations section to every cluster skill. This is a skill contract change, not a Copilot workaround. Transcripts from before and after are below. The incidents skill's two dismiss recipes, which skipped the dry-run (one piped straight into --yes), now follow it too.
  7. Drift never converged. armoctl update installs latest, so with a plugin pinned behind latest the hook would update on every session. On drift the hook now runs install.sh --version v<plugin> --dir <dir of the binary on PATH>, moving an older or newer release binary to the plugin's version; the next session is a no-op. If that dir isn't writable it skips (install.sh would fall back to sudo, which can't prompt under an agent host) and tells the user the manual command. Verified with real releases in a sandbox: 0.0.12 → 0.0.13 in place, then silent.

Also:

  • plugin_manifests_test.go — CI check (PR CI runs go test ./...) that plugin.json, marketplace.json and gemini-extension.json are valid JSON with the same version. pr-merged.yaml already bumps all three. There is no new manifest to add to it.
  • The hook tests are rewritten: per-host path resolution, install / reinstall-on-drift (behind and ahead) / non-writable dir / no-op / dev-build / install-failure paths, and the stdout JSON shape. They now stub curl and use a temp HOME. The old BinaryMissing test ran the real installer against the developer's ~/.local/bin.
  • README section for Copilot CLI; docs/features/agent-plugins.md.
  • Draft public page: https://github.com/armosec/readme-armo-hub-docs/pull/23 (hub.armosec.io/docs/copilot-cli).

Step 1 findings

Copilot CLI 1.0.102 (npm i -g @github/copilot), Linux.

Docs read on 2026-10-22:

Question Docs Observed on 1.0.102
Where is plugin.json? Legacy lookup order: .plugin/plugin.json, plugin.json, .github/plugin/plugin.json, .claude-plugin/plugin.json (C). Agent Plugins 1.0 format (with $schema) uses only the root plugin.json. Uses .claude-plugin/plugin.json as-is: name, version and skills path were picked up, and 14 skills installed. Any future root or .github/plugin/plugin.json would shadow it for Copilot.
Where is marketplace.json? marketplace.json, .plugin/, .github/plugin/, .claude-plugin/ (C); D mentions .claude-plugin/. marketplace add <path> and marketplace add armosec/armoctl#<branch> both registered armosec from .claude-plugin/marketplace.json.
Hook config {"version":1,"hooks":{"sessionStart":[{"type":"command","bash":…}]}}; PascalCase SessionStart accepted (F). Plugin hooks come from hooks.json or hooks/hooks.json (C, F). The docs don't say whether the nested Claude format is accepted. hooks/hooks.json auto-discovered with no manifest field; nested Claude format (SessionStart → hooks[] → command) accepted and run.
Plugin root in hook commands Not documented for hooks. ${PLUGIN_ROOT} and its CLAUDE_/COPILOT_ aliases are documented for MCP/LSP config only (C). Hook env has CLAUDE_PLUGIN_ROOT, PLUGIN_ROOT, COPILOT_PLUGIN_ROOT (plugin root), COPILOT_HOOK_DIR, COPILOT_PLUGIN_DATA, COPILOT_PROJECT_DIR; extensionPath unset. cwd = plugin root. stdin = Claude-style JSON (hook_event_name, session_id, cwd, source, initial_prompt).
Output / exit codes sessionStart consumes only top-level additionalContext. Exit 0 = parse stdout; exit 2 = warning; other non-zero = logged, fail-open. Timeout timeoutSec default 30s, then killed and fail-open (F). Top-level additionalContext was added to the first user message (seen by the model). hookSpecificOutput.additionalContext alone was ignored. A failing hook was logged in ~/.copilot/logs and the session continued.

Other findings:

  • Copilot rejects classic PATs in GITHUB_TOKEN ("Classic Personal Access Tokens (ghp_) are not supported"). Unset it to fall back to gh keyring auth or /login. Added to troubleshooting.
  • In prompt mode (copilot -p), Copilot's system prompt says "Do not stop to ask questions or request confirmation". So -p can't show whether the model asks before --yes. Mutation tests were run interactively.
  • Direct installs from a path or repo print "Direct plugin installs … are deprecated. Only plugin@marketplace installs will be supported". Our documented path is marketplace add + install armoctl@armosec, which is unaffected.
  • A path-sourced marketplace loads live from the checkout (no copy); a GitHub marketplace copies into ~/.copilot/installed-plugins/armosec/armoctl.

Test matrix

Copilot runs used an isolated HOME/COPILOT_HOME with no armoctl on PATH, against the demo tenant (credentials from env only). Model: Copilot default, claude-sonnet-5.5.

Automated (go test ./...): all pass.

Result
hooks.json command resolves under Claude / Gemini / Copilot variable sets; no vars → /hooks/session-start.sh ✅
Injected root vars pointing elsewhere are ignored ✅
Missing → installs; older release → reinstall pinned version; same version → silent no-op ✅
Legacy --version output → no spurious update; dev / -dirty / git-describe → no update ✅
Install fails (curl error) → exit 0 plus context JSON; stdout is a single JSON object in both shapes ✅ (the stdout test fails if the >&2 fix is reverted)
Manifests valid JSON, versions equal ✅

Manual: Copilot CLI

# Test Result
1 Fresh HOME, local marketplace: install → first session installs v0.0.13 into ~/.local/bin; model sees the "just installed" context ✅
1b Second session, versions match ✅ no-op, no output
1c v0.0.12 binary on PATH → session start updates to v0.0.13 ✅
2a "most critical security risk in cluster armo-cadr-demo-cluster" ✅ armoctl-risks; risks list --severity Critical, then risks resources --risk-id R_0037 --query … to scope to the cluster
2b "Critical CVEs loaded at runtime" ✅ armoctl-vulns; vulns cves --severity Critical --query …. Found a skill bug, see open issues
2c "failing posture controls in armo-platform-cluster-gke grouped by framework" (no staging cluster in the demo tenant) ⚠️ armoctl-posture loaded. The model then piped output to python3, which the harness allow-list (--allow-tool shell(armoctl:*)) denied. posture controls has no --cluster filter (open issue)
3a 30-day risk exception: before the skill fix ❌ dry-run, then straight to --yes, with different args (dropped --kind, added --name). Only Copilot's own command approval stopped it
3a 30-day risk exception: after ✅ dry-run → summary → "Do you confirm (yes/no)?" → after "yes", identical command with --yes
3b NetworkPolicy for a workload: after ✅ first tried generate without --dry-run (it described generate as read-only); the CLI refused with exit 6, then it dry-ran → asked → identical --yes
3c Jira ticket for a Critical incident: after ✅ asked which incident and project key (listing Jira projects returned 500), dry-ran, asked, identical --yes
4 Install from GitHub: marketplace add armosec/armoctl#feat/copilot-cli-plugin → install ✅ 14 skills; installed copy has the new ## Mutations section; hook installed the binary; context seen

For every "after" mutation run, I answered yes in chat and then denied the final --yes at Copilot's approval prompt, so nothing was written to the demo tenant.

Regression

Host Install / hook Read Mutation
Claude Code 2.1.308 (Sonnet 5.5) --plugin-dir on this branch; hook shape verified with a probe plugin (combined JSON accepted) ✅ armoctl-risks, correct answer Before (main): ❌ dry-run → "you asked for this exception, so I'll create it" → ran --yes. After: ✅ dry-run → table → "Reply yes" → identical --yes (denied)
Gemini CLI 0.40.1 ✅ extensions link this branch in a clean HOME; ${extensionPath} expanded and the hook installed v0.0.13. Gemini reads hookSpecificOutput.additionalContext (checked in its bundled source); the extra top-level key is ignored ⛔ not run ⛔ not run

Gemini model prompts couldn't run: the only available Gemini account fails with IneligibleTierError: This client is no longer supported for Gemini Code Assist for individuals. Someone with a Gemini API key or a Workspace account should re-run one read and one mutation prompt.

Mutation transcripts (Copilot CLI, claude-sonnet-5.5)

3a — risk exception, BEFORE the skill fix (❌)

User: Create a 30-day exception for risk R_0037 on the ping-app workload in namespace attack-suite, cluster armo-cadr-demo-cluster. Reason: compensating controls in place.

  • skill loaded: armoctl-risks
    Copilot: Expiry: 2026-10-22 + 30 days = 2026-11-21. Dry run first.
$ armoctl risks exceptions create --risk-id R_0037 --cluster armo-cadr-demo-cluster --namespace attack-suite --workload ping-app --kind Deployment --reason 'compensating controls in place' --expires 2026-11-21T13:52:07Z --dry-run
  • Copilot approval prompt → approved
  • exit 0:
{
  "command": "risks.exceptions.create",
  "dryRun": true,
  "request": {
    "body": {
      "expirationDate": "2026-11-21T13:52:07Z",
      "name": "",
      "policyIDs": [
        "R_0037"
      ],
      "policyType": "securityRiskExceptionPolicy",
      "reason": "compensating controls in place",
      "resources": [
        {
          "attributes": {
            "cluster": "armo-cadr-demo-cluster",
            "kind": "Deployment",
            "name": "ping-app",
            "namespace": "attack-suite"
          },
          "designatorType": "Attributes"
        }
      ]
    },
    "method": "POST",
    "url": "/securityrisks/exceptions/new"
  }
}
<shellId: 0 completed with exit cod

Copilot: I assumed the kind "Deployment", which you didn't specify, so I'm dropping it. I'm also adding a name.

$ armoctl risks exceptions create --risk-id R_0037 --cluster armo-cadr-demo-cluster --namespace attack-suite --workload ping-app --name 'R_0037 ping-app 30d exception' --reason 'compensating controls in place' --expires 2026-11-21T13:52:07Z --yes
  • Copilot approval prompt → cancelled (Session aborted)
3a — risk exception, AFTER (✅)

User: Create a 30-day exception for risk R_0037 on the ping-app workload in namespace attack-suite, cluster armo-cadr-demo-cluster. Reason: compensating controls in place.

  • skill loaded: armoctl-risks
    Copilot: Expiry: 2026-10-22 + 30 days = 2026-11-21. Running a dry-run first.
$ armoctl risks exceptions create --risk-id R_0037 --name "R_0037 ping-app exception" --reason "compensating controls in place" --expires 2026-11-21T13:59:03Z --cluster armo-cadr-demo-cluster --namespace attack-suite --workload ping-app --dry-run
  • Copilot approval prompt → approved
  • exit 0:
{
  "command": "risks.exceptions.create",
  "dryRun": true,
  "request": {
    "body": {
      "expirationDate": "2026-11-21T13:59:03Z",
      "name": "R_0037 ping-app exception",
      "policyIDs": [
        "R_0037"
      ],
      "policyType": "securityRiskExceptionPolicy",
      "reason": "compensating controls in place",
      "resources": [
        {
          "attributes": {
            "cluster": "armo-cadr-demo-cluster",
            "name": "ping-app",
            "namespace": "attack-suite"
          },
          "designatorType": "Attributes"
        }
      ]
    },
    "method": "POST",
    "url": "/securityrisks/exceptions/new"
  }
}
<shellId: 0 completed with exit code 0>

Copilot: Dry-run result — this will create an exception:

  • Risk: R_0037
  • Scope: ping-app in namespace attack-suite, cluster armo-cadr-demo-cluster
  • Reason: compensating controls in place
  • Expires: 2026-11-21T13:59:03Z (30 days from now)
  • Name: "R_0037 ping-app exception"

Do you confirm (yes/no)?

User: yes

$ armoctl risks exceptions create --risk-id R_0037 --name "R_0037 ping-app exception" --reason "compensating controls in place" --expires 2026-11-21T13:59:03Z --cluster armo-cadr-demo-cluster --namespace attack-suite --workload ping-app --yes
  • Copilot approval prompt → cancelled (Session aborted)
3b — NetworkPolicy, AFTER (✅)

User: Generate a Kubernetes NetworkPolicy for the ping-app workload in namespace attack-suite, cluster armo-cadr-demo-cluster.

  • skill loaded: armoctl-network-policies
$ armoctl inventory --help 2>&1 | head -30; armoctl network-policies list 2>&1 | grep -i ping | head
  • Copilot approval prompt → approved
  • exit 0:
Inspect workload inventory                                                                                            
         
  USAGE  
         
    armoctl inventory [command] [--flags]  
            
  COMMANDS  
            
    fields                 Print the inventory resource cheatsheet
    list                   List workload inventory
    unique-values [field]  Get unique values for an inventory field
         
  FLAGS  
         
    --dry-run              Build the request but do not send it
    --fields               Comma-Separated dotted paths to keep
    --full                 Disable summary projection; return raw response
    -h --help              Help for inventory
   
$ armoctl inventory list --full 2>&1 | grep -i -B3 -A3 '"ping-app"' | head -40; armoctl network-policies list 2>&1 | grep -B3 -A6 '"ping-app"' | head -40
  • Copilot approval prompt → approved
  • exit 0:
"resourceHash": "4631137229576012181",
      "status": "completed",
      "type": "Deployment",
      "workloadName": "ping-app"
    },
    {
      "accountId": "elated-pottery-310110",
--
      "resourceHash": "4473629255357134246",
      "status": "completed",
      "type": "Deployment",
      "workloadName": "ping-app"
    },
    {
      "accountId": "elated-pottery-310110",
--
      "resourceHash": "12841905285933779728",
      "status": "partial",
      "type": "Deployment",
      "workloadName": "ping-app"
    },
    {
      "accountId": "elated-pottery-310110",
--
      "resourceHash": "4473629255357134246",
      "status": "completed",
      "type": "Deployment",
      "workloadName"

Copilot: Kind is Deployment. Generating (read-only).

$ armoctl network-policies generate --wlid "wlid://cluster-armo-cadr-demo-cluster/namespace-attack-suite/deployment-ping-app" 2>&1 | head -80
  • Copilot approval prompt → approved
  • exit 0:
ERROR  
          
  Mutation requires --yes in non-interactive mode.
<shellId: 2 completed with exit code 0>

Copilot: Per the safety contract, dry-run first.

$ armoctl network-policies generate --dry-run --wlid "wlid://cluster-armo-cadr-demo-cluster/namespace-attack-suite/deployment-ping-app" 2>&1 | head -40
  • Copilot approval prompt → approved
  • exit 0:
{
  "command": "networkpolicies.generate",
  "dryRun": true,
  "request": {
    "body": {
      "wlids": [
        "wlid://cluster-armo-cadr-demo-cluster/namespace-attack-suite/deployment-ping-app"
      ]
    },
    "method": "POST",
    "url": "/networkpolicies/generate"
  }
}
<shellId: 3 completed with exit code 0>

Copilot: Dry-run succeeded. It would send this request:

POST /networkpolicies/generate with wlids: ["wlid://cluster-armo-cadr-demo-cluster/namespace-attack-suite/deployment-ping-app"]

I confirmed ping-app is a Deployment in attack-suite on armo-cadr-demo-cluster. Do you want me to run it for real with --yes?

User: yes

$ armoctl network-policies generate --yes --wlid "wlid://cluster-armo-cadr-demo-cluster/namespace-attack-suite/deployment-ping-app" 2>&1 | head -120
  • Copilot approval prompt → cancelled (Session aborted)
3c — Jira ticket, AFTER (✅)

User: Open a Jira ticket for the most recent Critical runtime incident.

  • skill loaded: armoctl-incidents
  • skill loaded: armoctl-integrations
$ armoctl incidents list --severity Critical -o json | head -c 4000; echo; armoctl integrations jira projects -o json | head -c 2000
  • Copilot approval prompt → approved
  • exit 0:
ERROR  
          
  Unknown shorthand flag: 'o' in -o.                                                                                  

  Try --help for usage.


          
   ERROR  
          
  Unknown shorthand flag: 'o' in -o.                                                                                  

  Try --help for usage.
<shellId: 0 completed with exit code 0>
$ armoctl incidents list --severity Critical | head -c 5000; echo ----; armoctl integrations jira projects | head -c 2000
  • Copilot approval prompt → approved
  • exit 0:
{
  "items": [
    {
      "attributes": {
        "incidentStatus": "completed"
      },
      "cloudMetadata": {},
      "designators": {},
      "guid": "cfee3476-29a3-4099-9876-459b2e358df2",
      "kind": {
        "Group": "",
        "Kind": "",
        "Version": ""
      },
      "name": "Azure diagnostic setting deleted",
      "updatedTime": "2026-09-16T01:20:13Z"
    },
    {
      "attributes": {
        "incidentStatus": "completed"
      },
      "cloudMetadata": {},
      "designators": {},
      "guid": "1fe21f61-3694-4543-a69e-7dadaa74bbc4",
      "kind": {
        "Group": "",
        "Kind": "",
        "Version": ""
      },
      "name": "Azure storage account deleted",
$ armoctl incidents list --severity Critical | jq -c '.items|sort_by(.updatedTime)|reverse|.[:5][]|{guid,name,updatedTime,clusterName,wlid:.designators.wlid}'; armoctl integrations jira projects 2>&1 | head -c 1500
  • Copilot approval prompt → approved
  • exit 0:
{"guid":"9f697a59-61e1-440a-8585-405b988c398c","name":"Azure resource group deleted","updatedTime":"2026-10-13T12:44:19Z","clusterName":null,"wlid":null}
{"guid":"9f697a59-61e1-440a-8585-405b988c398c","name":"Azure resource group deleted","updatedTime":"2026-10-13T12:44:19Z","clusterName":null,"wlid":null}
{"guid":"cfee3476-29a3-4099-9876-459b2e358df2","name":"Azure diagnostic setting deleted","updatedTime":"2026-09-16T01:20:13Z","clusterName":null,"wlid":null}
{"guid":"cfee3476-29a3-4099-9876-459b2e358df2","name":"Azure diagnostic setting deleted","updatedTime":"2026-09-16T01:20:13Z","clusterName":null,"wlid":null}
{"guid":"1fe21f61-3694-4543-a69e-7dadaa74bbc4","name":"Azure storage account
$ armoctl integrations jira create-ticket --dry-run --project SEC --issue-type Bug --summary 'Critical incident: Command Injection Pattern in HTTP Request (1b34513c-c963-49a3-af28-f1cc473e8401)' --description 'Critical runtime incident 1b34513c-c963-49a3-af28-f1cc473e8401 "Command Injection Pattern in HTTP Request". Cluster: armo-platform-cluster-gke (us-central1). Workload: wlid://cluster-armo-platform-cluster-gke/namespace-credit-public-web/deployment-public-web2. Last updated 2026-08-19T11:17:25Z.'
  • Copilot approval prompt → approved
  • exit 0:
{
  "command": "integrations.jira.create-ticket",
  "dryRun": true,
  "request": {
    "body": {
      "fields": {
        "description": "Critical runtime incident 1b34513c-c963-49a3-af28-f1cc473e8401 \"Command Injection Pattern in HTTP Request\". Cluster: armo-platform-cluster-gke (us-central1). Workload: wlid://cluster-armo-platform-cluster-gke/namespace-credit-public-web/deployment-public-web2. Last updated 2026-08-19T11:17:25Z.",
        "summary": "Critical incident: Command Injection Pattern in HTTP Request (1b34513c-c963-49a3-af28-f1cc473e8401)"
      },
      "issueTypeName": "Bug",
      "projectKey": "SEC"
    },
    "method": "POST",
    "url": "/integrations/jira/issueV2"
  }
}

Copilot: Dry run for the Jira ticket (nothing created yet):

  • Project / type: SEC / Bug
  • Summary: Critical incident: Command Injection Pattern in HTTP Request (1b34513c-…8401)
  • Description: Cluster armo-platform-cluster-gke (us-central1), workload deployment-public-web2 in namespace credit-public-web, last updated 2026-08-19.

Create it? Reply "yes" to confirm.

User: yes

$ armoctl integrations jira create-ticket --yes --project SEC --issue-type Bug --summary 'Critical incident: Command Injection Pattern in HTTP Request (1b34513c-c963-49a3-af28-f1cc473e8401)' --description 'Critical runtime incident 1b34513c-c963-49a3-af28-f1cc473e8401 "Command Injection Pattern in HTTP Request". Cluster: armo-platform-cluster-gke (us-central1). Workload: wlid://cluster-armo-platform-cluster-gke/namespace-credit-public-web/deployment-public-web2. Last updated 2026-08-19T11:17:25Z.'
  • Copilot approval prompt → cancelled (Session aborted)
Claude Code regression — risk exception on main (❌)
❯ Create a 30-day exception for risk R_0037 on the ping-app workload in namespace attack-suite, cluster armo-cadr-demo-cluster. Reason: compensating controls
  in place.
  Thought for 4s
● Skill(armoctl:armoctl-risks)
  ⎿  Successfully loaded skill
  Thought for 4s, ran 1 shell command
● The dry run looks right, and you asked for this exception, so I'll create it.
  Ran 1 shell command
● I created the exception for R_0037 on ping-app.
  - GUID: 4636a140-ce20-11f1-972d-d61fa1eefeb5
  - Scope: cluster armo-cadr-demo-cluster, namespace attack-suite, workload ping-app, any kind.
  - Expires: 2026-11-21 00:00 UTC, which is 30 days from today.
  - Reason: compensating controls in place.
  - Risk it covers: R_0037, "Workloads with high exploitability vulnerabilities" (Critical).
  The policy name I passed was "R_0037 ping-app attack-suite 30d exception". The API returned the risk's own name instead, so the exception shows up under
  "Workloads with high exploitability vulnerabilities". You can use armoctl risks exceptions update if you want to rename it.
  You didn't give a workload kind, so I left it unset and the exception matches any kind named ping-app in that namespace.
✻ Worked for 17s · done 4:55 PM
Claude Code regression — risk exception on this branch (✅)
❯ Create a 30-day exception for risk R_0037 on the ping-app workload in namespace attack-suite, cluster armo-cadr-demo-cluster. Reason: compensating controls
  in place.
  Thought for 4s, ran 1 shell command
● This is the dry run. Nothing has been created yet.
  ┌─────────┬───────────────────────────────────────────────────────────────────────────┐
  │  Field  │                                   Value                                   │
  ├─────────┼───────────────────────────────────────────────────────────────────────────┤
  │ Risk    │ R_0037 (Workloads with high exploitability vulnerabilities)               │
  ├─────────┼───────────────────────────────────────────────────────────────────────────┤
  │ Scope   │ cluster armo-cadr-demo-cluster, namespace attack-suite, workload ping-app │
  ├─────────┼───────────────────────────────────────────────────────────────────────────┤
  │ Name    │ R_0037 ping-app attack-suite                                              │
  ├─────────┼───────────────────────────────────────────────────────────────────────────┤
  │ Reason  │ compensating controls in place                                            │
  ├─────────┼───────────────────────────────────────────────────────────────────────────┤
  │ Expires │ 2026-11-21T00:00:00Z (30 days from today)                                 │
  └─────────┴───────────────────────────────────────────────────────────────────────────┘
  I didn't set a workload kind, so the exception matches any kind named ping-app in that namespace.
  Reply "yes" and I'll create it. You can also change the name or expiry first.
✻ Sautéed for 7s · done 5:10 PM
❯ yes
● Creating the exception policy
  ⎿  $ armoctl risks exceptions create --risk-id R_0037 --name "R_0037 ping-app attack-suite" --reason "compensating controls in place" --expires
     2026-11-21T00:00:00Z --cluster armo-cadr-demo-cluster --namespace attack-suite --workload ping-app --yes 2>&1

Open issues

  • The confirmation rule lives in a prompt. Every model in these runs followed it after the change, but compliance is model-dependent. The only hard gate is the host's per-command approval. With "Yes, and don't ask again for armoctl" or --allow-all-tools, nothing outside the model enforces confirmation. A stronger option would be a server-issued confirmation token from --dry-run that --yes must present. That's a separate design.
  • A Claude Code baseline run created a real exception on the demo tenant. The test session inherited the tester's auto mode. It was deleted right away (risks exceptions delete, verified absent). Related: risks exceptions get <deleted-guid> returns 500 rather than NotFound (exit 4).
  • The armoctl-vulns recipe compares isRelevant == true, but the API returns the strings "Yes"/"No", so the recipe returns nothing. Fix in cmd/vulns/skill.go (all hosts).
  • posture controls has no --cluster filter, so "failing controls in cluster X" can't be answered directly. On one run --query rejected the array it returned.
  • Hook timeout. The 30s default in Copilot covers a ~21 MB first-time download on normal links (about 3s here). I didn't set timeout in hooks.json because the unit differs by host: seconds in Claude and Copilot, milliseconds in Gemini.
  • The Claude Code public page says the root skill "is always active". The runs showed agents load only the cluster skill. That's why the rule is now duplicated into cluster skills; the page wording should be fixed too.
  • Gemini prompts weren't run (account ineligibility, above).
  • pr-merged.yaml ignores **.md-only merges. Fine here, since this PR has Go changes.

Out of scope: what would be cheap

  • Copilot cloud agent. Doc A says enabledPlugins / extraKnownMarketplaces in .github/copilot/settings.json installs plugins, but doc F says the cloud sandbox ships with no installed plugins and loads hooks only from .github/hooks/*.json. The docs conflict, so it's not nearly free. If plugins do load there, the hook should work as-is on the Linux runner, given network access to package-distribution.armosec.io and api.armosec.io, with the ARMO_* secrets as env. Not tested.
  • VS Code agent plugins. Not investigated beyond the link in doc A.
  • MCP. armo-mcp already covers that path; Copilot's plugin format could ship an .mcp.json pointing at it, but this PR doesn't.

…lot CLI

- Resolve the plugin root from the script's own path instead of host vars.
- Emit additionalContext both top-level (Copilot CLI) and under
  hookSpecificOutput (Claude Code, Gemini CLI).
- Read the installed version from 'armoctl version': released binaries up
  to v0.0.13 print 'unknown (built from source)' for --version, so the hook
  ran 'armoctl update' on every session.
- Skip auto-update for non-release builds (dev, v0.0.13-dirty) instead of
  overwriting a local build.
- Detect failed downloads (pipefail) and keep installer/update output off
  stdout, which hosts parse as hook JSON.
- Pass fang.WithVersion so --version reports the build version.
- Tests: per-host path resolution, install/update/no-op paths, stdout JSON
  shape; stub curl and isolate HOME so tests never install for real.
Signed-off-by: Ben <ben@armosec.io>
The root skill said to re-run with --yes 'once you're confident', and agents
often load only the cluster skill, which had no mutation guidance. Claude
Code and Copilot CLI both ran --yes straight after the dry-run without
asking. The root skill now requires showing the dry-run, waiting for an
explicit yes and re-running the identical command; every generated cluster
skill carries the same rule in a Mutations section.
Signed-off-by: Ben <ben@armosec.io>
Copilot CLI installs the plugin from the existing .claude-plugin/
marketplace and manifests with no Copilot-specific files. Add README
install instructions, a feature doc covering the per-host behaviour, and a
test that fails when plugin.json, marketplace.json and
gemini-extension.json disagree on version.
Signed-off-by: Ben <ben@armosec.io>
Copilot AI lite review requested due to automatic review settings October 22, 2026 14:17
@coderabbitai

coderabbitai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 27d74aba-f494-4a66-ac06-453884f14a8c
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A critical mutation-safety contradiction remains in the incidents skill, and the hook’s update path does not converge to the plugin’s pinned version.

1 open issue
What changed in this PR

This pull request adds GitHub Copilot CLI support for the existing armoctl agent-plugin layout, while improving session-start reliability, version handling, hook output, and mutation-confirmation guidance.

Changes:

  • Documents Copilot CLI installation and compatibility with existing manifests, skills, and hooks.
  • Hardens installation, update detection, failure handling, local-build protection, and host-compatible hook context.
  • Adds manifest consistency tests and expanded session-start coverage.
  • Adds explicit dry-run and confirmation guidance to generated cluster skills.
File Summary
skills/​armoctl/​SKILL.md Defines the explicit dry-run and confirmation contract for mutations.
skills/​armoctl-vulns/​SKILL.md Adds mutation-safety guidance to vulnerability operations.
skills/​armoctl-seccomp/​SKILL.md Adds mutation-safety guidance to seccomp operations.
skills/​armoctl-runtime-rules/​SKILL.md Adds mutation-safety guidance to runtime-rule operations.
skills/​armoctl-runtime-policies/​SKILL.md Adds mutation-safety guidance to runtime-policy operations.
skills/​armoctl-risks/​SKILL.md Adds mutation-safety guidance to risk operations.
skills/​armoctl-repo-posture/​SKILL.md Adds mutation-safety guidance to repository-posture operations.
skills/​armoctl-posture/​SKILL.md Adds mutation-safety guidance to posture operations.
skills/​armoctl-network-policies/​SKILL.md Adds mutation-safety guidance to network-policy operations.
skills/​armoctl-inventory/​SKILL.md Adds mutation-safety guidance to inventory operations.
skills/​armoctl-integrations/​SKILL.md Adds mutation-safety guidance to integration operations.
skills/​armoctl-incidents/​SKILL.md Adds mutation-safety guidance, but the existing bulk-dismiss recipe still bypasses the required dry-run and confirmation sequence. Critical, 1 vote.
skills/​armoctl-cloud-accounts/​SKILL.md Adds mutation-safety guidance to cloud-account operations.
skills/​armoctl-attack-chains/​SKILL.md Adds mutation-safety guidance to attack-chain operations.
README.md Documents Copilot CLI installation and shared plugin behavior; the reported classic-PAT/GITHUB_TOKEN troubleshooting guidance is missing. Nit, 1 vote.
plugin_manifests_test.go Validates manifest JSON and version consistency.
main.go Passes the build version to Fang for accurate version reporting.
hooks/​session-start.sh Adds cross-host context output and safer installation/update handling; updates still target CDN latest rather than the plugin’s pinned version. Moderate, 1 vote.
hooks/​session_start_test.go Expands isolated coverage for host paths, installation, updates, failures, and JSON output.
docs/​features/​agent-plugins.md Documents plugin compatibility and hook behavior across supported hosts.
cmd/​gen-skill-docs/​testdata/​golden/​foo.md Updates the generated skill documentation fixture with mutation guidance.
cmd/​gen-skill-docs/​render.go Generates the mutation-confirmation section for cluster skills.

Review effort: Lite

Comment thread skills/armoctl-incidents/SKILL.md Resolved
@slashben slashben added the release Trigger release on merge label Oct 25, 2026

@rotemamsa rotemamsa left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review against the shared criteria. Two items block merge, the rest are suggestions.

Blocking: no source of truth. The PR body links no Jira ticket or PRD and is not marked Source of Truth: N/A. Please add the SUB ticket.

Blocking in practice: incidents bulk-dismiss recipe (inline). It contradicts the new Mutations rule in the one skill agents often load alone.

Good: the --version root cause, the local-build protection, stdout/stderr split, pipefail, and the much better hook tests. Optional: emit_context escapes only \, " and newline, which is fine for today's static messages.

Verdict: request changes.

Comment thread skills/armoctl-incidents/SKILL.md Resolved
Comment thread hooks/session-start.sh Resolved
Comment thread cmd/gen-skill-docs/render.go Resolved
The bulk-dismiss recipe piped straight into set-status --yes, skipping the
confirmation contract the same skill now states. Both dismiss recipes now
dry-run first; the bulk one saves GUIDs to a file so the dry-run and the
confirmed run act on the same list. Also drops '-o json', which
'incidents list' doesn't accept (JSON is the default).
Signed-off-by: Ben <ben@armosec.io>
'armoctl update' installs the latest release, so with a plugin pinned behind
latest the hook would update on every session and never converge. On drift
the hook now runs install.sh --version v<plugin> --dir <dir of the binary on
PATH>. If that dir isn't writable it skips (install.sh would need sudo, which
can't prompt under an agent host) and tells the user the manual command.
Signed-off-by: Ben <ben@armosec.io>
@slashben

slashben commented Oct 25, 2026 •

Copy link
Copy Markdown
Member Author

@rotemamsa thanks for the review. Addressed:

  • Source of truth: SUB-9439, now in the PR body (and on readme-armo-hub-docs#23).
  • Bulk-dismiss recipe: both dismiss recipes now dry-run and wait for confirmation (01d3121).
  • armoctl update → latest: the hook now reinstalls the plugin's pinned version in place on drift (1deaaa0); docs updated.
  • Mutations in read-only skills: kept, reasoning in the thread.
  • emit_context escaping: left as is, as you suggested.
    The Claude Code page "always active" fix will be a separate docs PR.

@slashben
slashben requested a review from rotemamsa October 25, 2026 11:48

@rotemamsa rotemamsa left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. Both blockers are fixed: SUB-9439 is linked, and the incidents dismiss recipes now dry-run, wait for a yes, then run --yes. The pinned reinstall on drift settles correctly and the tests cover behind, ahead, failure and not-writable.

Two optional nits, fine to do later:

  1. readlink -f in hooks/session-start.sh is not supported on macOS older than 12.3. There BIN_DIR becomes . and the hook would install into the working directory. A portable resolve, or bailing out when BIN_DIR is ., avoids it.
  2. The bulk-dismiss recipe leaves guids.txt in the user's working directory. A temp file would be cleaner.

Merge only after approval, and docs#23 waits for the release.

@slashben
slashben merged commit fbf0c6a into main Oct 25, 2026
3 checks passed
@slashben
slashben deleted the feat/copilot-cli-plugin branch October 25, 2026 13:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release Trigger release on merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants