Repository navigation
feat: GitHub Copilot CLI support for the armoctl plugin - #39
Conversation
…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>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
There was a problem hiding this comment.
🟡 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
rotemamsa
left a comment
There was a problem hiding this comment.
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.
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>
|
@rotemamsa thanks for the review. Addressed:
|
rotemamsa
left a comment
There was a problem hiding this comment.
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:
readlink -finhooks/session-start.shis not supported on macOS older than 12.3. ThereBIN_DIRbecomes.and the hook would install into the working directory. A portable resolve, or bailing out whenBIN_DIRis., avoids it.- The bulk-dismiss recipe leaves
guids.txtin the user's working directory. A temp file would be cleaner.
Merge only after approval, and docs#23 waits for the release.

Source of Truth: SUB-9439
Summary
copilot plugin marketplace add armosec/armoctl+copilot plugin install armoctl@armosecnow gives Copilot CLI the same behaviour as Claude Code. No Copilot-specific manifest was needed: Copilot reads the existing.claude-plugin/files,skills/, andhooks/hooks.json.Contrary to the starting assumption, Copilot does expand the existing hook command. It exports
CLAUDE_PLUGIN_ROOTinto the hook environment, so${CLAUDE_PLUGIN_ROOT}${extensionPath}/hooks/session-start.shalready resolved. Testing turned up other problems, mostly not Copilot-specific, and this PR fixes them:additionalContext; the hook emitted onlyhookSpecificOutput.additionalContext(Claude/Gemini). It now emits both.armoctl updateon every session, in every host. Released binaries printarmoctl version unknown (built from source)for--version(fang falls back to Go build info becausefang.WithVersionwas never passed). The hook parsed the last word,source), which never matches. The hook now readsarmoctl version(which writes to stderr), andmain.gopassesfang.WithVersion(Version).~/.local/bin/armoctlwith the release. It now skips auto-update for non-release versions (dev,v0.0.13-dirty,v0.0.12-4-gabc123).install.shprints progress to stdout, which hosts parse as hook JSON, so the "just installed" message was dropped. Installer output now goes to stderr.curl … | bashwithout pipefail, a failed curl pipes nothing into bash, which exits 0. Fixed.--yeswithout 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-docsadds the same rule as a## Mutationssection 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.armoctl updateinstallslatest, so with a plugin pinned behind latest the hook would update on every session. On drift the hook now runsinstall.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 runsgo test ./...) thatplugin.json,marketplace.jsonandgemini-extension.jsonare valid JSON with the same version.pr-merged.yamlalready bumps all three. There is no new manifest to add to it.curland use a tempHOME. The oldBinaryMissingtest ran the real installer against the developer's~/.local/bin.docs/features/agent-plugins.md.Step 1 findings
Copilot CLI 1.0.102 (
npm i -g @github/copilot), Linux.Docs read on 2026-10-22:
plugin.json?.plugin/plugin.json,plugin.json,.github/plugin/plugin.json,.claude-plugin/plugin.json(C). Agent Plugins 1.0 format (with$schema) uses only the rootplugin.json..claude-plugin/plugin.jsonas-is: name, version andskillspath were picked up, and 14 skills installed. Any future root or.github/plugin/plugin.jsonwould shadow it for Copilot.marketplace.json?marketplace.json,.plugin/,.github/plugin/,.claude-plugin/(C); D mentions.claude-plugin/.marketplace add <path>andmarketplace add armosec/armoctl#<branch>both registeredarmosecfrom.claude-plugin/marketplace.json.{"version":1,"hooks":{"sessionStart":[{"type":"command","bash":…}]}}; PascalCaseSessionStartaccepted (F). Plugin hooks come fromhooks.jsonorhooks/hooks.json(C, F). The docs don't say whether the nested Claude format is accepted.hooks/hooks.jsonauto-discovered with no manifest field; nested Claude format (SessionStart→hooks[]→command) accepted and run.${PLUGIN_ROOT}and itsCLAUDE_/COPILOT_aliases are documented for MCP/LSP config only (C).CLAUDE_PLUGIN_ROOT,PLUGIN_ROOT,COPILOT_PLUGIN_ROOT(plugin root),COPILOT_HOOK_DIR,COPILOT_PLUGIN_DATA,COPILOT_PROJECT_DIR;extensionPathunset. cwd = plugin root. stdin = Claude-style JSON (hook_event_name,session_id,cwd,source,initial_prompt).additionalContext. Exit 0 = parse stdout; exit 2 = warning; other non-zero = logged, fail-open. TimeouttimeoutSecdefault 30s, then killed and fail-open (F).additionalContextwas added to the first user message (seen by the model).hookSpecificOutput.additionalContextalone was ignored. A failing hook was logged in~/.copilot/logsand the session continued.Other findings:
GITHUB_TOKEN("Classic Personal Access Tokens (ghp_) are not supported"). Unset it to fall back toghkeyring auth or/login. Added to troubleshooting.copilot -p), Copilot's system prompt says "Do not stop to ask questions or request confirmation". So-pcan't show whether the model asks before--yes. Mutation tests were run interactively.marketplace add+install armoctl@armosec, which is unaffected.~/.copilot/installed-plugins/armosec/armoctl.Test matrix
Copilot runs used an isolated
HOME/COPILOT_HOMEwith no armoctl onPATH, against the demo tenant (credentials from env only). Model: Copilot default, claude-sonnet-5.5.Automated (
go test ./...): all pass./hooks/session-start.sh--versionoutput → no spurious update;dev/-dirty/ git-describe → no update>&2fix is reverted)Manual: Copilot CLI
~/.local/bin; model sees the "just installed" contextarmoctl-risks;risks list --severity Critical, thenrisks resources --risk-id R_0037 --query …to scope to the clusterarmoctl-vulns;vulns cves --severity Critical --query …. Found a skill bug, see open issuesstagingcluster in the demo tenant)armoctl-postureloaded. The model then piped output topython3, which the harness allow-list (--allow-tool shell(armoctl:*)) denied.posture controlshas no--clusterfilter (open issue)--yes, with different args (dropped--kind, added--name). Only Copilot's own command approval stopped it--yesgeneratewithout--dry-run(it described generate as read-only); the CLI refused with exit 6, then it dry-ran → asked → identical--yes--yesmarketplace add armosec/armoctl#feat/copilot-cli-plugin→ install## Mutationssection; hook installed the binary; context seenFor every "after" mutation run, I answered yes in chat and then denied the final
--yesat Copilot's approval prompt, so nothing was written to the demo tenant.Regression
--plugin-diron this branch; hook shape verified with a probe plugin (combined JSON accepted)armoctl-risks, correct answermain): ❌ dry-run → "you asked for this exception, so I'll create it" → ran--yes. After: ✅ dry-run → table → "Reply yes" → identical--yes(denied)extensions linkthis branch in a clean HOME;${extensionPath}expanded and the hook installed v0.0.13. Gemini readshookSpecificOutput.additionalContext(checked in its bundled source); the extra top-level key is ignoredGemini 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.
armoctl-risksCopilot: Expiry: 2026-10-22 + 30 days = 2026-11-21. Dry run first.
Copilot: I assumed the kind "Deployment", which you didn't specify, so I'm dropping it. I'm also adding a name.
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.
armoctl-risksCopilot: Expiry: 2026-10-22 + 30 days = 2026-11-21. Running a dry-run first.
Copilot: Dry-run result — this will create an exception:
Do you confirm (yes/no)?
User: yes
3b — NetworkPolicy, AFTER (✅)
User: Generate a Kubernetes NetworkPolicy for the ping-app workload in namespace attack-suite, cluster armo-cadr-demo-cluster.
armoctl-network-policiesCopilot: Kind is Deployment. Generating (read-only).
Copilot: Per the safety contract, dry-run first.
Copilot: Dry-run succeeded. It would send this request:
POST /networkpolicies/generatewithwlids: ["wlid://cluster-armo-cadr-demo-cluster/namespace-attack-suite/deployment-ping-app"]I confirmed
ping-appis a Deployment inattack-suiteonarmo-cadr-demo-cluster. Do you want me to run it for real with--yes?User: yes
3c — Jira ticket, AFTER (✅)
User: Open a Jira ticket for the most recent Critical runtime incident.
armoctl-incidentsarmoctl-integrationsCopilot: Dry run for the Jira ticket (nothing created yet):
armo-platform-cluster-gke(us-central1), workloaddeployment-public-web2in namespacecredit-public-web, last updated 2026-08-19.Create it? Reply "yes" to confirm.
User: yes
Claude Code regression — risk exception on main (❌)
Claude Code regression — risk exception on this branch (✅)
Open issues
armoctl" or--allow-all-tools, nothing outside the model enforces confirmation. A stronger option would be a server-issued confirmation token from--dry-runthat--yesmust present. That's a separate design.risks exceptions delete, verified absent). Related:risks exceptions get <deleted-guid>returns 500 rather than NotFound (exit 4).armoctl-vulnsrecipe comparesisRelevant == true, but the API returns the strings"Yes"/"No", so the recipe returns nothing. Fix incmd/vulns/skill.go(all hosts).posture controlshas no--clusterfilter, so "failing controls in cluster X" can't be answered directly. On one run--queryrejected the array it returned.timeoutin hooks.json because the unit differs by host: seconds in Claude and Copilot, milliseconds in Gemini.pr-merged.yamlignores**.md-only merges. Fine here, since this PR has Go changes.Out of scope: what would be cheap
enabledPlugins/extraKnownMarketplacesin.github/copilot/settings.jsoninstalls 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 topackage-distribution.armosec.ioandapi.armosec.io, with theARMO_*secrets as env. Not tested..mcp.jsonpointing at it, but this PR doesn't.