You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@magpie.apache.org by po...@apache.org on 2026/07/08 17:13:58 UTC
(magpie) branch main updated: feat(adapter/kiro): first-class Kiro CLI skill runtime (#768)
This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/magpie.git
The following commit(s) were added to refs/heads/main by this push:
new a3f92b4d feat(adapter/kiro): first-class Kiro CLI skill runtime (#768)
a3f92b4d is described below
commit a3f92b4d37338e85ff60ea05acfe1917c9e100d4
Author: Alex Guglielmone Nemi <al...@gmail.com>
AuthorDate: Wed Jul 8 18:13:52 2026 +0100
feat(adapter/kiro): first-class Kiro CLI skill runtime (#768)
Bring Kiro CLI (the rebranded Amazon Q Developer CLI — #320) to parity with
Claude Code and OpenCode as a skill runtime, following the
docs/adapters/add-a-harness.md recipe (#741).
Discovery: Kiro reads .kiro/skills/ (not .agents/skills/), so add a `kiro` row
to the skills/setup/agents.md registry plus the per-skill relays; the
registry-driven adopt/verify/worktree-init logic then wires it automatically.
.agents/.github/.kiro all carry the 69 magpie-* relays.
Enforcement (agent-harness score: Kiro now 5/5 substrate tools):
- agent-guard: a --kiro preToolUse adapter (kiro_main) over the shared
dispatch() core — Kiro feeds {tool_name, tool_input.command, cwd} on stdin
and blocks on exit 2 with the reason on stderr.
- sandbox-lint --kiro and permission-audit audit-kiro: encode Kiro's
allowlist/denylist model (allowedCommands/deniedCommands = \A..\z regex,
deny-before-allow, denyByDefault) and flag configs that auto-approve
dangerous shell / exfil surfaces.
- agent-isolation kiro-iso: the env -i credential-strip launcher (the harness
name `kiro` normalises to the `kiro-cli` binary).
- spec-loop: a headless `kiro-cli chat --no-interactive` runner profile in lib.sh.
Verified: full prek --all-files; per-tool pytest + ruff + mypy; the spec-loop
runner fixture (kiro argv); symlink-lint (relays cycle-free + target-correct);
discovery — a fresh kiro-cli discovers all skills with the relays present and 0
without (negative control); agent-guard end-to-end — a real .kiro preToolUse
hook blocks a Co-Authored-By commit and allows a clean one;
vendor-neutrality-score regenerates with Kiro at 5/5.
Known limitations: agent-isolation's bubblewrap OS-sandbox layer is unit-tested
for the launcher but its e2e is deferred to a bwrap-capable host (as OpenCode's
is); a general /magpie-setup auto-installer for non-Claude-only harness
enforcement is a follow-up (Claude-only today), so the Kiro hook uses
${MAGPIE_AGENT_GUARD:-.claude/hooks/agent-guard.py}.
Generated-by: Kiro CLI (Opus 4.8)
Co-authored-by: Jarek Potiuk <ja...@potiuk.com>
---
.gitignore | 2 +
.kiro/skills/magpie-audit-finding-fix | 1 +
.kiro/skills/magpie-ci-runner-audit | 1 +
.kiro/skills/magpie-committer-onboarding | 1 +
.kiro/skills/magpie-contributor-activity-sweep | 1 +
.kiro/skills/magpie-contributor-nomination | 1 +
.kiro/skills/magpie-contributor-sentiment | 1 +
.kiro/skills/magpie-contributor-to-committer | 1 +
.kiro/skills/magpie-dependency-audit | 1 +
.kiro/skills/magpie-flaky-test-triage | 1 +
.kiro/skills/magpie-good-first-issue-author | 1 +
.kiro/skills/magpie-good-first-issue-sweep | 1 +
.kiro/skills/magpie-issue-backlog-stats | 1 +
.kiro/skills/magpie-issue-deduplicate | 1 +
.kiro/skills/magpie-issue-fix-workflow | 1 +
.kiro/skills/magpie-issue-reassess | 1 +
.kiro/skills/magpie-issue-reassess-stats | 1 +
.kiro/skills/magpie-issue-reproducer | 1 +
.kiro/skills/magpie-issue-stale-sweep | 1 +
.kiro/skills/magpie-issue-triage | 1 +
.kiro/skills/magpie-license-compliance-audit | 1 +
.kiro/skills/magpie-list-skills | 1 +
.kiro/skills/magpie-mentoring-welcome | 1 +
.kiro/skills/magpie-newcomer-issue-explainer | 1 +
.kiro/skills/magpie-onboarding-concierge | 1 +
.kiro/skills/magpie-optimize-skill | 1 +
.kiro/skills/magpie-pairing-multi-agent-review | 1 +
.kiro/skills/magpie-pairing-self-review | 1 +
.kiro/skills/magpie-pr-management-code-review | 1 +
.kiro/skills/magpie-pr-management-mentor | 1 +
.kiro/skills/magpie-pr-management-quick-merge | 1 +
.kiro/skills/magpie-pr-management-stats | 1 +
.kiro/skills/magpie-pr-management-triage | 1 +
.kiro/skills/magpie-pr-stale-sweep | 1 +
.kiro/skills/magpie-pre-first-pr-check | 1 +
.kiro/skills/magpie-release-announce-draft | 1 +
.kiro/skills/magpie-release-archive-sweep | 1 +
.kiro/skills/magpie-release-audit-report | 1 +
.kiro/skills/magpie-release-keys-sync | 1 +
.kiro/skills/magpie-release-prepare | 1 +
.kiro/skills/magpie-release-promote | 1 +
.kiro/skills/magpie-release-rc-cut | 1 +
.kiro/skills/magpie-release-verify-rc | 1 +
.kiro/skills/magpie-release-vote-draft | 1 +
.kiro/skills/magpie-release-vote-tally | 1 +
.kiro/skills/magpie-reviewer-routing | 1 +
.kiro/skills/magpie-security-cve-allocate | 1 +
.kiro/skills/magpie-security-issue-deduplicate | 1 +
.kiro/skills/magpie-security-issue-fix | 1 +
.kiro/skills/magpie-security-issue-import | 1 +
.kiro/skills/magpie-security-issue-import-from-md | 1 +
.kiro/skills/magpie-security-issue-import-from-pr | 1 +
.../skills/magpie-security-issue-import-from-scan | 1 +
.../magpie-security-issue-import-via-forwarder | 1 +
.kiro/skills/magpie-security-issue-invalidate | 1 +
.kiro/skills/magpie-security-issue-sync | 1 +
.kiro/skills/magpie-security-issue-triage | 1 +
.../skills/magpie-security-tracker-stats-dashboard | 1 +
.kiro/skills/magpie-setup | 1 +
.kiro/skills/magpie-setup-isolated-setup-doctor | 1 +
.kiro/skills/magpie-setup-isolated-setup-install | 1 +
.kiro/skills/magpie-setup-isolated-setup-update | 1 +
.kiro/skills/magpie-setup-isolated-setup-verify | 1 +
.kiro/skills/magpie-setup-override-upstream | 1 +
.kiro/skills/magpie-setup-shared-config-sync | 1 +
.kiro/skills/magpie-setup-status | 1 +
.kiro/skills/magpie-setup-upstream-fix | 1 +
.kiro/skills/magpie-skill-reconciler | 1 +
.kiro/skills/magpie-workflow-security-audit | 1 +
.kiro/skills/magpie-write-skill | 1 +
docs/vendor-neutrality.md | 9 +-
skills/setup/agents.md | 1 +
tools/agent-guard/README.md | 46 +++++-
tools/agent-guard/src/agent_guard/__init__.py | 36 +++++
tools/agent-guard/tests/test_kiro.py | 106 +++++++++++++
tools/agent-isolation/README.md | 5 +-
tools/agent-isolation/agent-iso.sh | 6 +
tools/agent-isolation/tests/test_kiro_iso.py | 153 +++++++++++++++++++
tools/permission-audit/README.md | 25 ++-
tools/permission-audit/src/permission_audit/cli.py | 24 +++
.../permission-audit/src/permission_audit/kiro.py | 170 +++++++++++++++++++++
tools/permission-audit/tests/test_kiro.py | 114 ++++++++++++++
tools/sandbox-lint/README.md | 21 ++-
tools/sandbox-lint/src/sandbox_lint/__init__.py | 28 ++++
tools/sandbox-lint/src/sandbox_lint/kiro.py | 148 ++++++++++++++++++
tools/sandbox-lint/tests/test_kiro.py | 120 +++++++++++++++
tools/spec-loop/README.md | 2 +-
tools/spec-loop/lib.sh | 6 +
tools/spec-loop/loop.sh | 1 +
tools/spec-loop/specs/spec-loop-runner.md | 5 +-
tools/spec-loop/tests/test_runner_fixtures.sh | 8 +
91 files changed, 1094 insertions(+), 11 deletions(-)
diff --git a/.gitignore b/.gitignore
index 1f2e8ae2..d0d597ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,8 @@ logs
!/.agents/skills/magpie-*
.claude/skills/*
!/.claude/skills/magpie-*
+.kiro/skills/*
+!/.kiro/skills/magpie-*
.github/skills/*
!/.github/skills/magpie-*
diff --git a/.kiro/skills/magpie-audit-finding-fix b/.kiro/skills/magpie-audit-finding-fix
new file mode 120000
index 00000000..aadc23dc
--- /dev/null
+++ b/.kiro/skills/magpie-audit-finding-fix
@@ -0,0 +1 @@
+../../.agents/skills/magpie-audit-finding-fix
\ No newline at end of file
diff --git a/.kiro/skills/magpie-ci-runner-audit b/.kiro/skills/magpie-ci-runner-audit
new file mode 120000
index 00000000..da506b8e
--- /dev/null
+++ b/.kiro/skills/magpie-ci-runner-audit
@@ -0,0 +1 @@
+../../.agents/skills/magpie-ci-runner-audit
\ No newline at end of file
diff --git a/.kiro/skills/magpie-committer-onboarding b/.kiro/skills/magpie-committer-onboarding
new file mode 120000
index 00000000..062b79bc
--- /dev/null
+++ b/.kiro/skills/magpie-committer-onboarding
@@ -0,0 +1 @@
+../../.agents/skills/magpie-committer-onboarding
\ No newline at end of file
diff --git a/.kiro/skills/magpie-contributor-activity-sweep b/.kiro/skills/magpie-contributor-activity-sweep
new file mode 120000
index 00000000..20f8f7a7
--- /dev/null
+++ b/.kiro/skills/magpie-contributor-activity-sweep
@@ -0,0 +1 @@
+../../.agents/skills/magpie-contributor-activity-sweep
\ No newline at end of file
diff --git a/.kiro/skills/magpie-contributor-nomination b/.kiro/skills/magpie-contributor-nomination
new file mode 120000
index 00000000..33297c55
--- /dev/null
+++ b/.kiro/skills/magpie-contributor-nomination
@@ -0,0 +1 @@
+../../.agents/skills/magpie-contributor-nomination
\ No newline at end of file
diff --git a/.kiro/skills/magpie-contributor-sentiment b/.kiro/skills/magpie-contributor-sentiment
new file mode 120000
index 00000000..c2083842
--- /dev/null
+++ b/.kiro/skills/magpie-contributor-sentiment
@@ -0,0 +1 @@
+../../.agents/skills/magpie-contributor-sentiment
\ No newline at end of file
diff --git a/.kiro/skills/magpie-contributor-to-committer b/.kiro/skills/magpie-contributor-to-committer
new file mode 120000
index 00000000..74a0c9a3
--- /dev/null
+++ b/.kiro/skills/magpie-contributor-to-committer
@@ -0,0 +1 @@
+../../.agents/skills/magpie-contributor-to-committer
\ No newline at end of file
diff --git a/.kiro/skills/magpie-dependency-audit b/.kiro/skills/magpie-dependency-audit
new file mode 120000
index 00000000..22e94f89
--- /dev/null
+++ b/.kiro/skills/magpie-dependency-audit
@@ -0,0 +1 @@
+../../.agents/skills/magpie-dependency-audit
\ No newline at end of file
diff --git a/.kiro/skills/magpie-flaky-test-triage b/.kiro/skills/magpie-flaky-test-triage
new file mode 120000
index 00000000..7fe30ec6
--- /dev/null
+++ b/.kiro/skills/magpie-flaky-test-triage
@@ -0,0 +1 @@
+../../.agents/skills/magpie-flaky-test-triage
\ No newline at end of file
diff --git a/.kiro/skills/magpie-good-first-issue-author b/.kiro/skills/magpie-good-first-issue-author
new file mode 120000
index 00000000..bdbc0eb3
--- /dev/null
+++ b/.kiro/skills/magpie-good-first-issue-author
@@ -0,0 +1 @@
+../../.agents/skills/magpie-good-first-issue-author
\ No newline at end of file
diff --git a/.kiro/skills/magpie-good-first-issue-sweep b/.kiro/skills/magpie-good-first-issue-sweep
new file mode 120000
index 00000000..916fb3e7
--- /dev/null
+++ b/.kiro/skills/magpie-good-first-issue-sweep
@@ -0,0 +1 @@
+../../.agents/skills/magpie-good-first-issue-sweep
\ No newline at end of file
diff --git a/.kiro/skills/magpie-issue-backlog-stats b/.kiro/skills/magpie-issue-backlog-stats
new file mode 120000
index 00000000..304b94c6
--- /dev/null
+++ b/.kiro/skills/magpie-issue-backlog-stats
@@ -0,0 +1 @@
+../../.agents/skills/magpie-issue-backlog-stats
\ No newline at end of file
diff --git a/.kiro/skills/magpie-issue-deduplicate b/.kiro/skills/magpie-issue-deduplicate
new file mode 120000
index 00000000..534240ba
--- /dev/null
+++ b/.kiro/skills/magpie-issue-deduplicate
@@ -0,0 +1 @@
+../../.agents/skills/magpie-issue-deduplicate
\ No newline at end of file
diff --git a/.kiro/skills/magpie-issue-fix-workflow b/.kiro/skills/magpie-issue-fix-workflow
new file mode 120000
index 00000000..8668b7d2
--- /dev/null
+++ b/.kiro/skills/magpie-issue-fix-workflow
@@ -0,0 +1 @@
+../../.agents/skills/magpie-issue-fix-workflow
\ No newline at end of file
diff --git a/.kiro/skills/magpie-issue-reassess b/.kiro/skills/magpie-issue-reassess
new file mode 120000
index 00000000..d0b1057c
--- /dev/null
+++ b/.kiro/skills/magpie-issue-reassess
@@ -0,0 +1 @@
+../../.agents/skills/magpie-issue-reassess
\ No newline at end of file
diff --git a/.kiro/skills/magpie-issue-reassess-stats b/.kiro/skills/magpie-issue-reassess-stats
new file mode 120000
index 00000000..be4558b0
--- /dev/null
+++ b/.kiro/skills/magpie-issue-reassess-stats
@@ -0,0 +1 @@
+../../.agents/skills/magpie-issue-reassess-stats
\ No newline at end of file
diff --git a/.kiro/skills/magpie-issue-reproducer b/.kiro/skills/magpie-issue-reproducer
new file mode 120000
index 00000000..1f8dbd24
--- /dev/null
+++ b/.kiro/skills/magpie-issue-reproducer
@@ -0,0 +1 @@
+../../.agents/skills/magpie-issue-reproducer
\ No newline at end of file
diff --git a/.kiro/skills/magpie-issue-stale-sweep b/.kiro/skills/magpie-issue-stale-sweep
new file mode 120000
index 00000000..18b02038
--- /dev/null
+++ b/.kiro/skills/magpie-issue-stale-sweep
@@ -0,0 +1 @@
+../../.agents/skills/magpie-issue-stale-sweep
\ No newline at end of file
diff --git a/.kiro/skills/magpie-issue-triage b/.kiro/skills/magpie-issue-triage
new file mode 120000
index 00000000..98fa1b1f
--- /dev/null
+++ b/.kiro/skills/magpie-issue-triage
@@ -0,0 +1 @@
+../../.agents/skills/magpie-issue-triage
\ No newline at end of file
diff --git a/.kiro/skills/magpie-license-compliance-audit b/.kiro/skills/magpie-license-compliance-audit
new file mode 120000
index 00000000..dbd4526a
--- /dev/null
+++ b/.kiro/skills/magpie-license-compliance-audit
@@ -0,0 +1 @@
+../../.agents/skills/magpie-license-compliance-audit
\ No newline at end of file
diff --git a/.kiro/skills/magpie-list-skills b/.kiro/skills/magpie-list-skills
new file mode 120000
index 00000000..86c5d5c8
--- /dev/null
+++ b/.kiro/skills/magpie-list-skills
@@ -0,0 +1 @@
+../../.agents/skills/magpie-list-skills
\ No newline at end of file
diff --git a/.kiro/skills/magpie-mentoring-welcome b/.kiro/skills/magpie-mentoring-welcome
new file mode 120000
index 00000000..22681849
--- /dev/null
+++ b/.kiro/skills/magpie-mentoring-welcome
@@ -0,0 +1 @@
+../../.agents/skills/magpie-mentoring-welcome
\ No newline at end of file
diff --git a/.kiro/skills/magpie-newcomer-issue-explainer b/.kiro/skills/magpie-newcomer-issue-explainer
new file mode 120000
index 00000000..eb7da31f
--- /dev/null
+++ b/.kiro/skills/magpie-newcomer-issue-explainer
@@ -0,0 +1 @@
+../../.agents/skills/magpie-newcomer-issue-explainer
\ No newline at end of file
diff --git a/.kiro/skills/magpie-onboarding-concierge b/.kiro/skills/magpie-onboarding-concierge
new file mode 120000
index 00000000..b8933747
--- /dev/null
+++ b/.kiro/skills/magpie-onboarding-concierge
@@ -0,0 +1 @@
+../../.agents/skills/magpie-onboarding-concierge
\ No newline at end of file
diff --git a/.kiro/skills/magpie-optimize-skill b/.kiro/skills/magpie-optimize-skill
new file mode 120000
index 00000000..38928cca
--- /dev/null
+++ b/.kiro/skills/magpie-optimize-skill
@@ -0,0 +1 @@
+../../.agents/skills/magpie-optimize-skill
\ No newline at end of file
diff --git a/.kiro/skills/magpie-pairing-multi-agent-review b/.kiro/skills/magpie-pairing-multi-agent-review
new file mode 120000
index 00000000..aefce05d
--- /dev/null
+++ b/.kiro/skills/magpie-pairing-multi-agent-review
@@ -0,0 +1 @@
+../../.agents/skills/magpie-pairing-multi-agent-review
\ No newline at end of file
diff --git a/.kiro/skills/magpie-pairing-self-review b/.kiro/skills/magpie-pairing-self-review
new file mode 120000
index 00000000..db045e21
--- /dev/null
+++ b/.kiro/skills/magpie-pairing-self-review
@@ -0,0 +1 @@
+../../.agents/skills/magpie-pairing-self-review
\ No newline at end of file
diff --git a/.kiro/skills/magpie-pr-management-code-review b/.kiro/skills/magpie-pr-management-code-review
new file mode 120000
index 00000000..f75f26f6
--- /dev/null
+++ b/.kiro/skills/magpie-pr-management-code-review
@@ -0,0 +1 @@
+../../.agents/skills/magpie-pr-management-code-review
\ No newline at end of file
diff --git a/.kiro/skills/magpie-pr-management-mentor b/.kiro/skills/magpie-pr-management-mentor
new file mode 120000
index 00000000..5c6e19f3
--- /dev/null
+++ b/.kiro/skills/magpie-pr-management-mentor
@@ -0,0 +1 @@
+../../.agents/skills/magpie-pr-management-mentor
\ No newline at end of file
diff --git a/.kiro/skills/magpie-pr-management-quick-merge b/.kiro/skills/magpie-pr-management-quick-merge
new file mode 120000
index 00000000..04b43f10
--- /dev/null
+++ b/.kiro/skills/magpie-pr-management-quick-merge
@@ -0,0 +1 @@
+../../.agents/skills/magpie-pr-management-quick-merge
\ No newline at end of file
diff --git a/.kiro/skills/magpie-pr-management-stats b/.kiro/skills/magpie-pr-management-stats
new file mode 120000
index 00000000..b247b1cf
--- /dev/null
+++ b/.kiro/skills/magpie-pr-management-stats
@@ -0,0 +1 @@
+../../.agents/skills/magpie-pr-management-stats
\ No newline at end of file
diff --git a/.kiro/skills/magpie-pr-management-triage b/.kiro/skills/magpie-pr-management-triage
new file mode 120000
index 00000000..9ac1831e
--- /dev/null
+++ b/.kiro/skills/magpie-pr-management-triage
@@ -0,0 +1 @@
+../../.agents/skills/magpie-pr-management-triage
\ No newline at end of file
diff --git a/.kiro/skills/magpie-pr-stale-sweep b/.kiro/skills/magpie-pr-stale-sweep
new file mode 120000
index 00000000..0a8f6fa6
--- /dev/null
+++ b/.kiro/skills/magpie-pr-stale-sweep
@@ -0,0 +1 @@
+../../.agents/skills/magpie-pr-stale-sweep
\ No newline at end of file
diff --git a/.kiro/skills/magpie-pre-first-pr-check b/.kiro/skills/magpie-pre-first-pr-check
new file mode 120000
index 00000000..e4d6eacc
--- /dev/null
+++ b/.kiro/skills/magpie-pre-first-pr-check
@@ -0,0 +1 @@
+../../.agents/skills/magpie-pre-first-pr-check
\ No newline at end of file
diff --git a/.kiro/skills/magpie-release-announce-draft b/.kiro/skills/magpie-release-announce-draft
new file mode 120000
index 00000000..2aece0be
--- /dev/null
+++ b/.kiro/skills/magpie-release-announce-draft
@@ -0,0 +1 @@
+../../.agents/skills/magpie-release-announce-draft
\ No newline at end of file
diff --git a/.kiro/skills/magpie-release-archive-sweep b/.kiro/skills/magpie-release-archive-sweep
new file mode 120000
index 00000000..8237f536
--- /dev/null
+++ b/.kiro/skills/magpie-release-archive-sweep
@@ -0,0 +1 @@
+../../.agents/skills/magpie-release-archive-sweep
\ No newline at end of file
diff --git a/.kiro/skills/magpie-release-audit-report b/.kiro/skills/magpie-release-audit-report
new file mode 120000
index 00000000..59ea4e5e
--- /dev/null
+++ b/.kiro/skills/magpie-release-audit-report
@@ -0,0 +1 @@
+../../.agents/skills/magpie-release-audit-report
\ No newline at end of file
diff --git a/.kiro/skills/magpie-release-keys-sync b/.kiro/skills/magpie-release-keys-sync
new file mode 120000
index 00000000..e4a62ba5
--- /dev/null
+++ b/.kiro/skills/magpie-release-keys-sync
@@ -0,0 +1 @@
+../../.agents/skills/magpie-release-keys-sync
\ No newline at end of file
diff --git a/.kiro/skills/magpie-release-prepare b/.kiro/skills/magpie-release-prepare
new file mode 120000
index 00000000..e10457f7
--- /dev/null
+++ b/.kiro/skills/magpie-release-prepare
@@ -0,0 +1 @@
+../../.agents/skills/magpie-release-prepare
\ No newline at end of file
diff --git a/.kiro/skills/magpie-release-promote b/.kiro/skills/magpie-release-promote
new file mode 120000
index 00000000..fcd25c30
--- /dev/null
+++ b/.kiro/skills/magpie-release-promote
@@ -0,0 +1 @@
+../../.agents/skills/magpie-release-promote
\ No newline at end of file
diff --git a/.kiro/skills/magpie-release-rc-cut b/.kiro/skills/magpie-release-rc-cut
new file mode 120000
index 00000000..605e1861
--- /dev/null
+++ b/.kiro/skills/magpie-release-rc-cut
@@ -0,0 +1 @@
+../../.agents/skills/magpie-release-rc-cut
\ No newline at end of file
diff --git a/.kiro/skills/magpie-release-verify-rc b/.kiro/skills/magpie-release-verify-rc
new file mode 120000
index 00000000..50bb2497
--- /dev/null
+++ b/.kiro/skills/magpie-release-verify-rc
@@ -0,0 +1 @@
+../../.agents/skills/magpie-release-verify-rc
\ No newline at end of file
diff --git a/.kiro/skills/magpie-release-vote-draft b/.kiro/skills/magpie-release-vote-draft
new file mode 120000
index 00000000..78c69713
--- /dev/null
+++ b/.kiro/skills/magpie-release-vote-draft
@@ -0,0 +1 @@
+../../.agents/skills/magpie-release-vote-draft
\ No newline at end of file
diff --git a/.kiro/skills/magpie-release-vote-tally b/.kiro/skills/magpie-release-vote-tally
new file mode 120000
index 00000000..2aff4878
--- /dev/null
+++ b/.kiro/skills/magpie-release-vote-tally
@@ -0,0 +1 @@
+../../.agents/skills/magpie-release-vote-tally
\ No newline at end of file
diff --git a/.kiro/skills/magpie-reviewer-routing b/.kiro/skills/magpie-reviewer-routing
new file mode 120000
index 00000000..89dfff49
--- /dev/null
+++ b/.kiro/skills/magpie-reviewer-routing
@@ -0,0 +1 @@
+../../.agents/skills/magpie-reviewer-routing
\ No newline at end of file
diff --git a/.kiro/skills/magpie-security-cve-allocate b/.kiro/skills/magpie-security-cve-allocate
new file mode 120000
index 00000000..921523b9
--- /dev/null
+++ b/.kiro/skills/magpie-security-cve-allocate
@@ -0,0 +1 @@
+../../.agents/skills/magpie-security-cve-allocate
\ No newline at end of file
diff --git a/.kiro/skills/magpie-security-issue-deduplicate b/.kiro/skills/magpie-security-issue-deduplicate
new file mode 120000
index 00000000..61f19ec3
--- /dev/null
+++ b/.kiro/skills/magpie-security-issue-deduplicate
@@ -0,0 +1 @@
+../../.agents/skills/magpie-security-issue-deduplicate
\ No newline at end of file
diff --git a/.kiro/skills/magpie-security-issue-fix b/.kiro/skills/magpie-security-issue-fix
new file mode 120000
index 00000000..993c5cc1
--- /dev/null
+++ b/.kiro/skills/magpie-security-issue-fix
@@ -0,0 +1 @@
+../../.agents/skills/magpie-security-issue-fix
\ No newline at end of file
diff --git a/.kiro/skills/magpie-security-issue-import b/.kiro/skills/magpie-security-issue-import
new file mode 120000
index 00000000..5a6b3b5e
--- /dev/null
+++ b/.kiro/skills/magpie-security-issue-import
@@ -0,0 +1 @@
+../../.agents/skills/magpie-security-issue-import
\ No newline at end of file
diff --git a/.kiro/skills/magpie-security-issue-import-from-md b/.kiro/skills/magpie-security-issue-import-from-md
new file mode 120000
index 00000000..64df4d75
--- /dev/null
+++ b/.kiro/skills/magpie-security-issue-import-from-md
@@ -0,0 +1 @@
+../../.agents/skills/magpie-security-issue-import-from-md
\ No newline at end of file
diff --git a/.kiro/skills/magpie-security-issue-import-from-pr b/.kiro/skills/magpie-security-issue-import-from-pr
new file mode 120000
index 00000000..7f0ad364
--- /dev/null
+++ b/.kiro/skills/magpie-security-issue-import-from-pr
@@ -0,0 +1 @@
+../../.agents/skills/magpie-security-issue-import-from-pr
\ No newline at end of file
diff --git a/.kiro/skills/magpie-security-issue-import-from-scan b/.kiro/skills/magpie-security-issue-import-from-scan
new file mode 120000
index 00000000..aa6fd9e7
--- /dev/null
+++ b/.kiro/skills/magpie-security-issue-import-from-scan
@@ -0,0 +1 @@
+../../.agents/skills/magpie-security-issue-import-from-scan
\ No newline at end of file
diff --git a/.kiro/skills/magpie-security-issue-import-via-forwarder b/.kiro/skills/magpie-security-issue-import-via-forwarder
new file mode 120000
index 00000000..0f29ded6
--- /dev/null
+++ b/.kiro/skills/magpie-security-issue-import-via-forwarder
@@ -0,0 +1 @@
+../../.agents/skills/magpie-security-issue-import-via-forwarder
\ No newline at end of file
diff --git a/.kiro/skills/magpie-security-issue-invalidate b/.kiro/skills/magpie-security-issue-invalidate
new file mode 120000
index 00000000..0bd6690f
--- /dev/null
+++ b/.kiro/skills/magpie-security-issue-invalidate
@@ -0,0 +1 @@
+../../.agents/skills/magpie-security-issue-invalidate
\ No newline at end of file
diff --git a/.kiro/skills/magpie-security-issue-sync b/.kiro/skills/magpie-security-issue-sync
new file mode 120000
index 00000000..6408ae08
--- /dev/null
+++ b/.kiro/skills/magpie-security-issue-sync
@@ -0,0 +1 @@
+../../.agents/skills/magpie-security-issue-sync
\ No newline at end of file
diff --git a/.kiro/skills/magpie-security-issue-triage b/.kiro/skills/magpie-security-issue-triage
new file mode 120000
index 00000000..ae7d4bbb
--- /dev/null
+++ b/.kiro/skills/magpie-security-issue-triage
@@ -0,0 +1 @@
+../../.agents/skills/magpie-security-issue-triage
\ No newline at end of file
diff --git a/.kiro/skills/magpie-security-tracker-stats-dashboard b/.kiro/skills/magpie-security-tracker-stats-dashboard
new file mode 120000
index 00000000..57883a5b
--- /dev/null
+++ b/.kiro/skills/magpie-security-tracker-stats-dashboard
@@ -0,0 +1 @@
+../../.agents/skills/magpie-security-tracker-stats-dashboard
\ No newline at end of file
diff --git a/.kiro/skills/magpie-setup b/.kiro/skills/magpie-setup
new file mode 120000
index 00000000..625c6aa3
--- /dev/null
+++ b/.kiro/skills/magpie-setup
@@ -0,0 +1 @@
+../../.agents/skills/magpie-setup
\ No newline at end of file
diff --git a/.kiro/skills/magpie-setup-isolated-setup-doctor b/.kiro/skills/magpie-setup-isolated-setup-doctor
new file mode 120000
index 00000000..ab0d0fb0
--- /dev/null
+++ b/.kiro/skills/magpie-setup-isolated-setup-doctor
@@ -0,0 +1 @@
+../../.agents/skills/magpie-setup-isolated-setup-doctor
\ No newline at end of file
diff --git a/.kiro/skills/magpie-setup-isolated-setup-install b/.kiro/skills/magpie-setup-isolated-setup-install
new file mode 120000
index 00000000..0de94144
--- /dev/null
+++ b/.kiro/skills/magpie-setup-isolated-setup-install
@@ -0,0 +1 @@
+../../.agents/skills/magpie-setup-isolated-setup-install
\ No newline at end of file
diff --git a/.kiro/skills/magpie-setup-isolated-setup-update b/.kiro/skills/magpie-setup-isolated-setup-update
new file mode 120000
index 00000000..f9907e1a
--- /dev/null
+++ b/.kiro/skills/magpie-setup-isolated-setup-update
@@ -0,0 +1 @@
+../../.agents/skills/magpie-setup-isolated-setup-update
\ No newline at end of file
diff --git a/.kiro/skills/magpie-setup-isolated-setup-verify b/.kiro/skills/magpie-setup-isolated-setup-verify
new file mode 120000
index 00000000..929dcc24
--- /dev/null
+++ b/.kiro/skills/magpie-setup-isolated-setup-verify
@@ -0,0 +1 @@
+../../.agents/skills/magpie-setup-isolated-setup-verify
\ No newline at end of file
diff --git a/.kiro/skills/magpie-setup-override-upstream b/.kiro/skills/magpie-setup-override-upstream
new file mode 120000
index 00000000..27fbec7a
--- /dev/null
+++ b/.kiro/skills/magpie-setup-override-upstream
@@ -0,0 +1 @@
+../../.agents/skills/magpie-setup-override-upstream
\ No newline at end of file
diff --git a/.kiro/skills/magpie-setup-shared-config-sync b/.kiro/skills/magpie-setup-shared-config-sync
new file mode 120000
index 00000000..9c9224e7
--- /dev/null
+++ b/.kiro/skills/magpie-setup-shared-config-sync
@@ -0,0 +1 @@
+../../.agents/skills/magpie-setup-shared-config-sync
\ No newline at end of file
diff --git a/.kiro/skills/magpie-setup-status b/.kiro/skills/magpie-setup-status
new file mode 120000
index 00000000..4eaf51a2
--- /dev/null
+++ b/.kiro/skills/magpie-setup-status
@@ -0,0 +1 @@
+../../.agents/skills/magpie-setup-status
\ No newline at end of file
diff --git a/.kiro/skills/magpie-setup-upstream-fix b/.kiro/skills/magpie-setup-upstream-fix
new file mode 120000
index 00000000..6779e068
--- /dev/null
+++ b/.kiro/skills/magpie-setup-upstream-fix
@@ -0,0 +1 @@
+../../.agents/skills/magpie-setup-upstream-fix
\ No newline at end of file
diff --git a/.kiro/skills/magpie-skill-reconciler b/.kiro/skills/magpie-skill-reconciler
new file mode 120000
index 00000000..64156835
--- /dev/null
+++ b/.kiro/skills/magpie-skill-reconciler
@@ -0,0 +1 @@
+../../.agents/skills/magpie-skill-reconciler
\ No newline at end of file
diff --git a/.kiro/skills/magpie-workflow-security-audit b/.kiro/skills/magpie-workflow-security-audit
new file mode 120000
index 00000000..bb5f28d4
--- /dev/null
+++ b/.kiro/skills/magpie-workflow-security-audit
@@ -0,0 +1 @@
+../../.agents/skills/magpie-workflow-security-audit
\ No newline at end of file
diff --git a/.kiro/skills/magpie-write-skill b/.kiro/skills/magpie-write-skill
new file mode 120000
index 00000000..23274dfc
--- /dev/null
+++ b/.kiro/skills/magpie-write-skill
@@ -0,0 +1 @@
+../../.agents/skills/magpie-write-skill
\ No newline at end of file
diff --git a/docs/vendor-neutrality.md b/docs/vendor-neutrality.md
index bc0dfaca..bfb26311 100644
--- a/docs/vendor-neutrality.md
+++ b/docs/vendor-neutrality.md
@@ -583,24 +583,24 @@ Organization scope (declared, orthogonal to vendor): ASF = 14, agnostic = 55.
| Substrate tool | Substrate | Harness support | Verdict |
|---|---|---|---|
-| `agent-guard` | action-guard | Claude Code, OpenCode | ✅ portable |
+| `agent-guard` | action-guard | Claude Code, Kiro, OpenCode | ✅ portable |
| `agent-isolation` | sandbox | any | ✅ agnostic |
| `dashboard-generator` | analytics | any | ✅ agnostic |
| `dev` | framework-dev | any | ✅ agnostic |
| `egress-gateway` | sandbox | any | ✅ agnostic |
-| `permission-audit` | sandbox | Claude Code, OpenCode | ✅ portable |
+| `permission-audit` | sandbox | Claude Code, Kiro, OpenCode | ✅ portable |
| `pilot-report-validator` | framework-dev | any | ✅ agnostic |
| `pr-management-stats` | analytics | any | ✅ agnostic |
| `preflight-audit` | analytics | any | ✅ agnostic |
| `privacy-llm` | privacy | any | ✅ agnostic |
| `probe-templates` | sandbox | any | ✅ agnostic |
-| `sandbox-lint` | sandbox | Claude Code, OpenCode | ✅ portable |
+| `sandbox-lint` | sandbox | Claude Code, Kiro, OpenCode | ✅ portable |
| `security-tracker-stats-dashboard` | analytics | any | ✅ agnostic |
| `skill-and-tool-validator` | framework-dev | any | ✅ agnostic |
| `skill-evals` | framework-dev | any | ✅ agnostic |
| `skill-reconciler-diff` | framework-dev | any | ✅ agnostic |
| `spec-inventory` | framework-dev, analytics | any | ✅ agnostic |
-| `spec-loop` | framework-dev | Claude Code, Codex, Cursor, Gemini CLI, OpenCode | ✅ portable |
+| `spec-loop` | framework-dev | Claude Code, Codex, Cursor, Gemini CLI, Kiro, OpenCode | ✅ portable |
| `spec-status-index` | framework-dev, analytics | any | ✅ agnostic |
| `spec-validator` | framework-dev | any | ✅ agnostic |
| `symlink-lint` | framework-dev | any | ✅ agnostic |
@@ -612,6 +612,7 @@ Harness → substrate tools it supports:
- **Codex** (1): `spec-loop`
- **Cursor** (1): `spec-loop`
- **Gemini CLI** (1): `spec-loop`
+- **Kiro** (4): `agent-guard`, `permission-audit`, `sandbox-lint`, `spec-loop`
- **OpenCode** (4): `agent-guard`, `permission-audit`, `sandbox-lint`, `spec-loop`
- **any harness** (18): `agent-isolation`, `dashboard-generator`, `dev`, `egress-gateway`, `pilot-report-validator`, `pr-management-stats`, `preflight-audit`, `privacy-llm`, `probe-templates`, `security-tracker-stats-dashboard`, `skill-and-tool-validator`, `skill-evals`, `skill-reconciler-diff`, `spec-inventory`, `spec-status-index`, `spec-validator`, `symlink-lint`, `vendor-neutrality-score`
diff --git a/skills/setup/agents.md b/skills/setup/agents.md
index f8210e6d..bc870ddf 100644
--- a/skills/setup/agents.md
+++ b/skills/setup/agents.md
@@ -27,6 +27,7 @@ go"*, never as *"what files contain"*.
| `github` | `.github/skills/` | native (relay) | GitHub's skill loader |
| `windsurf` | `.windsurf/skills/` | native (relay) | Windsurf |
| `goose` | `.goose/skills/` | native (relay) | Goose |
+| `kiro` | `.kiro/skills/` | native (relay) | Kiro CLI |
The table is **extensible**: a new agent that wants framework
skills is one new row (`id`, project dir, kind), nothing else —
diff --git a/tools/agent-guard/README.md b/tools/agent-guard/README.md
index a72c27cd..c4135388 100644
--- a/tools/agent-guard/README.md
+++ b/tools/agent-guard/README.md
@@ -11,6 +11,7 @@
- [Per-command overrides](#per-command-overrides)
- [Wiring](#wiring)
- [OpenCode](#opencode)
+ - [Kiro CLI](#kiro-cli)
- [Harness-neutral path (any runtime)](#harness-neutral-path-any-runtime)
- [Contributing guards](#contributing-guards)
- [Tests](#tests)
@@ -24,7 +25,7 @@
**Capability:** substrate:action-guard
-**Harness:** Claude Code, OpenCode
+**Harness:** Claude Code, OpenCode, Kiro
A deterministic pre-execution guard dispatcher. It inspects every shell command
**before it runs** and **denies** the ones that would break a hard framework
@@ -40,6 +41,9 @@ so every wired harness enforces an identical rule set from one source of truth:
- **OpenCode** — a [plugin](https://opencode.ai/docs/plugins/) on the
`tool.execute.before` hook for the `bash` tool, which blocks a call by
throwing (`agent-guard.py --opencode`). See [Wiring](#wiring).
+- **Kiro CLI** — a [`preToolUse`](https://kiro.dev/docs/cli/hooks) hook on the
+ `execute_bash` matcher, which blocks a call when the hook exits `2`
+ (`agent-guard.py --kiro`, reason on stderr). See [Wiring](#wiring).
- **Any other runtime** — the `--check` and `--exec` CLI modes let any
harness or shell wrapper enforce guard rules without a harness-specific hook
adapter. See [Harness-neutral path (any runtime)](#harness-neutral-path-any-runtime).
@@ -145,6 +149,46 @@ to point elsewhere. Because both harnesses call `dispatch()`, the bundled and
skill-contributed guards, the `MAGPIE_*` overrides, and the deny reasons are
byte-for-byte identical across the two; nothing about a guard is harness-aware.
+### Kiro CLI
+
+The same engine backs [Kiro CLI](https://kiro.dev/docs/cli/hooks) via its
+`preToolUse` hook. Kiro pipes the hook event
+(`{"tool_name": "execute_bash", "tool_input": {"command": …}, "cwd": …}`) to
+the hook command on stdin and **blocks** the tool call when that command exits
+`2`, returning its stderr to the model — so `agent-guard.py --kiro` matches the
+shell tool, runs the shared core, and on a deny writes the reason to stderr and
+exits `2`, the Kiro equivalent of a Claude `PreToolUse` deny.
+
+Register it in the agent configuration's `hooks` field
+([reference](https://kiro.dev/docs/cli/custom-agents/configuration-reference#hooks-field)):
+
+```json
+{
+ "hooks": {
+ "preToolUse": [
+ {
+ "matcher": "execute_bash",
+ "command": "python3 \"${MAGPIE_AGENT_GUARD:-.claude/hooks/agent-guard.py}\" --kiro"
+ }
+ ]
+ }
+}
+```
+
+The engine is one shared, harness-agnostic file (`--kiro` only selects the I/O
+adapter). On a repo already wired for Claude Code it lives at
+`.claude/hooks/agent-guard.py`, so the hook reuses it; a **Kiro-only** adopter
+(no `.claude/`) points `MAGPIE_AGENT_GUARD` at wherever the engine is installed.
+Note: `/magpie-setup` currently installs the engine only on the Claude path — a
+general installer that drops it for non-Claude-only adopters is a pending
+follow-up (see [`docs/adapters/add-a-harness.md`](../../docs/adapters/add-a-harness.md)).
+
+Because every harness calls `dispatch()`, the bundled and skill-contributed
+guards, the `MAGPIE_*` overrides, and the deny reasons are identical to the
+Claude Code and OpenCode paths — verified end-to-end: with this hook wired,
+Kiro refuses a `Co-Authored-By` commit (quoting the `commit-trailer` reason and
+leaving the commit uncreated) while a clean commit proceeds.
+
### Harness-neutral path (any runtime)
For runtimes that do not expose a pre-tool hook API (Codex CLI, Gemini CLI,
diff --git a/tools/agent-guard/src/agent_guard/__init__.py b/tools/agent-guard/src/agent_guard/__init__.py
index 6cb25db2..3f28dfb1 100644
--- a/tools/agent-guard/src/agent_guard/__init__.py
+++ b/tools/agent-guard/src/agent_guard/__init__.py
@@ -581,6 +581,39 @@ def opencode_main() -> int:
return ALLOW_EXIT
+def kiro_main() -> int:
+ """Harness-neutral entry point for Kiro CLI's ``preToolUse`` hook.
+
+ Reads Kiro's hook event on stdin — ``{"tool_name": "...", "tool_input":
+ {"command": "..."}, "cwd": "..."}`` — matches the shell tool
+ (``execute_bash`` / its ``shell`` alias / ``execute_cmd``), and runs the
+ **same** :func:`dispatch` core as every other harness. On a deny it writes
+ the reason to **stderr** (which Kiro returns to the model) and exits
+ ``DENY_EXIT`` (2 = block for ``PreToolUse``); on allow, a non-shell tool, or
+ any malformed input it exits ``ALLOW_EXIT`` — fail-open, exactly like the
+ Claude and OpenCode paths. Only this thin I/O shell differs; the guard
+ decisions are identical across harnesses.
+ """
+ try:
+ event = json.load(sys.stdin)
+ except (json.JSONDecodeError, ValueError):
+ return ALLOW_EXIT
+ if not isinstance(event, dict):
+ return ALLOW_EXIT
+ if event.get("tool_name") not in ("execute_bash", "shell", "execute_cmd"):
+ return ALLOW_EXIT
+ tool_input = event.get("tool_input")
+ if not isinstance(tool_input, dict):
+ return ALLOW_EXIT
+ command = str(tool_input.get("command", ""))
+ cwd = event.get("cwd")
+ reason = dispatch(command, cwd if isinstance(cwd, str) else None)
+ if reason:
+ sys.stderr.write(reason + "\n")
+ return DENY_EXIT
+ return ALLOW_EXIT
+
+
def check_main(argv: list[str]) -> int:
"""Harness-neutral check-only entry point (``--check``).
@@ -698,6 +731,7 @@ def cli(argv: list[str] | None = None) -> int:
No argument → the Claude Code ``PreToolUse`` hook (:func:`main`).
``--opencode`` → the OpenCode adapter (:func:`opencode_main`).
+ ``--kiro`` → the Kiro CLI adapter (:func:`kiro_main`).
``--check <cmd…>`` → harness-neutral check-only (:func:`check_main`).
``--exec <cmd…>`` → harness-neutral check-then-exec (:func:`exec_main`).
@@ -708,6 +742,8 @@ def cli(argv: list[str] | None = None) -> int:
args = sys.argv[1:] if argv is None else argv
if args and args[0] == "--opencode":
return opencode_main()
+ if args and args[0] == "--kiro":
+ return kiro_main()
if args and args[0] == "--check":
return check_main(args[1:])
if args and args[0] == "--exec":
diff --git a/tools/agent-guard/tests/test_kiro.py b/tools/agent-guard/tests/test_kiro.py
new file mode 100644
index 00000000..8759ae45
--- /dev/null
+++ b/tools/agent-guard/tests/test_kiro.py
@@ -0,0 +1,106 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Tests for the Kiro CLI adapter entry point.
+
+Kiro's ``preToolUse`` hook feeds ``{"tool_name", "tool_input": {"command"},
+"cwd"}`` on stdin and blocks a tool call on exit code 2 with the reason on
+STDERR. These tests drive the ``--kiro`` I/O shell and assert it reaches the
+same guard decisions as the shared :func:`agent_guard.dispatch` core that
+backs the Claude Code hook and the OpenCode adapter.
+"""
+
+from __future__ import annotations
+
+import io
+import json
+
+import pytest
+
+from agent_guard import ALLOW_EXIT, DENY_EXIT, kiro_main, opencode_main
+
+
+def _feed(monkeypatch: pytest.MonkeyPatch, payload: object) -> None:
+ text = payload if isinstance(payload, str) else json.dumps(payload)
+ monkeypatch.setattr("sys.stdin", io.StringIO(text))
+
+
+def _event(command: str, tool_name: str = "execute_bash") -> dict:
+ return {
+ "hook_event_name": "preToolUse",
+ "cwd": ".",
+ "tool_name": tool_name,
+ "tool_input": {"command": command},
+ }
+
+
+def test_denied_command_exits_deny_with_reason_on_stderr(
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
+) -> None:
+ # A Co-Authored-By trailer is one of the bundled deny rules.
+ _feed(monkeypatch, _event("git commit -m 'x\n\nCo-Authored-By: A <a...@b.c>'"))
+ rc = kiro_main()
+ captured = capsys.readouterr()
+ assert rc == DENY_EXIT
+ # Kiro surfaces STDERR (not STDOUT) to the model.
+ assert "commit-trailer" in captured.err
+ assert captured.out == ""
+
+
+def test_allowed_command_exits_allow_silently(
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
+) -> None:
+ _feed(monkeypatch, _event("git status"))
+ rc = kiro_main()
+ captured = capsys.readouterr()
+ assert rc == ALLOW_EXIT
+ assert captured.out == "" and captured.err == ""
+
+
+def test_shell_alias_is_matched(monkeypatch: pytest.MonkeyPatch) -> None:
+ _feed(monkeypatch, _event("git commit -m 'x\n\nCo-Authored-By: A <a...@b.c>'", tool_name="shell"))
+ assert kiro_main() == DENY_EXIT
+
+
+def test_execute_cmd_alias_is_matched(monkeypatch: pytest.MonkeyPatch) -> None:
+ _feed(monkeypatch, _event("git commit -m 'x\n\nCo-Authored-By: A <a...@b.c>'", tool_name="execute_cmd"))
+ assert kiro_main() == DENY_EXIT
+
+
+def test_non_shell_tool_is_fast_path_allow(monkeypatch: pytest.MonkeyPatch) -> None:
+ _feed(monkeypatch, {"tool_name": "fs_read", "tool_input": {"path": "/x"}})
+ assert kiro_main() == ALLOW_EXIT
+
+
+def test_malformed_stdin_fails_open(monkeypatch: pytest.MonkeyPatch) -> None:
+ _feed(monkeypatch, "not json {{{")
+ assert kiro_main() == ALLOW_EXIT
+
+
+def test_missing_tool_input_allows(monkeypatch: pytest.MonkeyPatch) -> None:
+ _feed(monkeypatch, {"tool_name": "execute_bash"})
+ assert kiro_main() == ALLOW_EXIT
+
+
+def test_verdict_matches_opencode_core(monkeypatch: pytest.MonkeyPatch) -> None:
+ """Differential: --kiro and --opencode reach the same dispatch() verdict."""
+ cmd = "git commit -m 'x\n\nCo-Authored-By: A <a...@b.c>'"
+ _feed(monkeypatch, _event(cmd))
+ kiro_rc = kiro_main()
+ _feed(monkeypatch, {"command": cmd})
+ opencode_rc = opencode_main()
+ assert kiro_rc == opencode_rc == DENY_EXIT
diff --git a/tools/agent-isolation/README.md b/tools/agent-isolation/README.md
index ff7ac565..e5a140ad 100644
--- a/tools/agent-isolation/README.md
+++ b/tools/agent-isolation/README.md
@@ -30,13 +30,16 @@ plain shell scripts plus a TOML manifest of pinned upstream
versions.
The clean-environment launcher [`agent-iso.sh`](agent-iso.sh) provides
-harness-agnostic env isolation. It exposes three entry points, all sharing
+harness-agnostic env isolation. It exposes four entry points, all sharing
the same `env -i` credential-strip core:
- **`claude-iso`** — launches Claude Code; additionally injects a one-shot
`--settings` sandbox `allowRead` grant for the current repo (Claude-specific).
- **`opencode-iso`** — launches OpenCode with the same clean env; no settings
grant (OpenCode takes its filesystem isolation from the OS-level sandbox).
+- **`kiro-iso`** — launches Kiro CLI (the `kiro` name normalises to its
+ `kiro-cli` binary) with the same clean env; like OpenCode, it takes its
+ filesystem isolation from the OS-level sandbox (no `--settings` grant).
- **`agent-iso <cli>`** — launches *any* agentic CLI (`codex`, `cursor`,
`gemini`, `aider`, …) with the same credential strip. The `--settings`
injection is skipped for non-Claude CLIs, which take their filesystem
diff --git a/tools/agent-isolation/agent-iso.sh b/tools/agent-isolation/agent-iso.sh
index c6aea685..d076b93a 100755
--- a/tools/agent-isolation/agent-iso.sh
+++ b/tools/agent-isolation/agent-iso.sh
@@ -80,6 +80,10 @@
# `claude-iso` / `opencode-iso` entry points are thin wrappers over this.
agent_iso_run() {
local agent="$1"
+ # The Kiro harness is invoked as `kiro` but ships the `kiro-cli` binary;
+ # normalise so both `AGENT_ISO_AGENT=kiro` and the `kiro-iso` entry point
+ # resolve the real executable. (Claude/OpenCode names match their binaries.)
+ [[ "$agent" == "kiro" ]] && agent="kiro-cli"
shift
# Resolve the agent binary on PATH before clobbering the env so
@@ -309,6 +313,7 @@ claude_iso_main() { agent_iso_run "${AGENT_ISO_AGENT:-claude}" "$@"; }
if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then
claude-iso() { agent_iso_run claude "$@"; }
opencode-iso() { agent_iso_run opencode "$@"; }
+ kiro-iso() { agent_iso_run kiro "$@"; }
# Harness-agnostic entry point: agent-iso <cli> [cli-args]
# Guard the no-CLI case so it matches the direct-exec path (usage + exit 1)
# instead of falling through to agent_iso_run with an empty agent name.
@@ -323,6 +328,7 @@ else
_aig_basename="$(basename "${0}")"
case "$_aig_basename" in
opencode-iso*) agent_iso_run opencode "$@" ;;
+ kiro-iso*) agent_iso_run kiro "$@" ;;
# Symlink named exactly `agent-iso` (no .sh extension) → first positional
# arg is the harness CLI name.
agent-iso)
diff --git a/tools/agent-isolation/tests/test_kiro_iso.py b/tools/agent-isolation/tests/test_kiro_iso.py
new file mode 100644
index 00000000..292bb960
--- /dev/null
+++ b/tools/agent-isolation/tests/test_kiro_iso.py
@@ -0,0 +1,153 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Tests for the Kiro entry point of agent-iso.sh (`kiro-iso`).
+
+Same strategy as ``test_opencode_iso.py``, with one Kiro-specific wrinkle:
+the harness is invoked as ``kiro`` but ships the ``kiro-cli`` binary, so the
+wrapper normalises the name. The fake binary is therefore ``kiro-cli``, and
+the banner/error use ``kiro-cli-iso``. Verifies the clean-env strip and that
+the Claude-only ``--settings`` injection is not applied.
+
+(The bubblewrap OS sandbox layer is not exercised here — it is unavailable in
+this environment; this test covers the ``env -i`` credential-strip launcher,
+which is the harness-portable part.)
+"""
+
+from __future__ import annotations
+
+import os
+import shutil
+import stat
+import subprocess
+from pathlib import Path
+
+SCRIPT = Path(__file__).parent.parent / "agent-iso.sh"
+BASH = shutil.which("bash") or "/bin/bash"
+
+
+def _make_fake_kiro(tmp_path: Path) -> Path:
+ """A fake 'kiro-cli' that prints ARG: lines for its argv, then its env."""
+ fake = tmp_path / "kiro-cli"
+ fake.write_text('#!/bin/sh\nfor a in "$@"; do echo "ARG:$a"; done\nprintenv\n')
+ fake.chmod(fake.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
+ return tmp_path
+
+
+def _run_kiro(
+ tmp_path: Path,
+ extra_env: dict | None = None,
+ extra_args: list | None = None,
+ via_symlink: bool = False,
+) -> subprocess.CompletedProcess:
+ bin_dir = _make_fake_kiro(tmp_path)
+ env: dict[str, str] = {
+ "PATH": f"{bin_dir}:{os.environ.get('PATH', '/usr/bin:/bin')}",
+ "HOME": "/tmp/testhome",
+ "USER": "testuser",
+ "SHELL": "/bin/sh",
+ "TERM": "xterm",
+ "LANG": "en_US.UTF-8",
+ }
+ if not via_symlink:
+ # The harness name `kiro` must normalise to the `kiro-cli` binary.
+ env["AGENT_ISO_AGENT"] = "kiro"
+ if extra_env:
+ env.update(extra_env)
+
+ script: Path = SCRIPT
+ if via_symlink:
+ link = tmp_path / "kiro-iso.sh"
+ if not link.exists():
+ link.symlink_to(SCRIPT)
+ script = link
+
+ return subprocess.run(
+ [BASH, str(script)] + (extra_args or []),
+ env=env,
+ cwd=str(tmp_path),
+ capture_output=True,
+ text=True,
+ )
+
+
+def _parse_env(stdout: str) -> dict[str, str]:
+ result: dict[str, str] = {}
+ for line in stdout.splitlines():
+ if line.startswith("ARG:"):
+ continue
+ if "=" in line:
+ key, _, val = line.partition("=")
+ result[key] = val
+ return result
+
+
+def _argv(stdout: str) -> list[str]:
+ return [
+ line[len("ARG:") :] for line in stdout.splitlines() if line.startswith("ARG:")
+ ]
+
+
+class TestKiroAgent:
+ def test_launches_kiro_cli_binary(self, tmp_path: Path) -> None:
+ res = _run_kiro(tmp_path)
+ assert res.returncode == 0
+ assert "[kiro-cli-iso] running in isolated env" in res.stderr
+
+ def test_credentials_stripped(self, tmp_path: Path) -> None:
+ res = _run_kiro(
+ tmp_path,
+ extra_env={"GH_TOKEN": "ghp_secret", "ANTHROPIC_API_KEY": "sk-ant-secret"},
+ )
+ assert res.returncode == 0
+ env = _parse_env(res.stdout)
+ assert "GH_TOKEN" not in env
+ assert "ANTHROPIC_API_KEY" not in env
+
+ def test_passthrough_preserved(self, tmp_path: Path) -> None:
+ res = _run_kiro(tmp_path, extra_env={"HOME": "/home/testuser"})
+ assert res.returncode == 0
+ assert _parse_env(res.stdout).get("HOME") == "/home/testuser"
+
+ def test_no_claude_settings_injection(self, tmp_path: Path) -> None:
+ res = _run_kiro(tmp_path, extra_args=["chat", "--no-interactive", "hello"])
+ assert res.returncode == 0
+ argv = _argv(res.stdout)
+ assert "--settings" not in argv
+ assert argv == ["chat", "--no-interactive", "hello"]
+
+ def test_symlink_name_selects_kiro(self, tmp_path: Path) -> None:
+ res = _run_kiro(tmp_path, via_symlink=True)
+ assert res.returncode == 0
+ assert "[kiro-cli-iso] running in isolated env" in res.stderr
+
+ def test_missing_kiro_exits_127(self, tmp_path: Path) -> None:
+ result = subprocess.run(
+ [BASH, str(SCRIPT)],
+ env={
+ "PATH": str(tmp_path), # no kiro-cli binary here
+ "HOME": "/tmp",
+ "USER": "testuser",
+ "SHELL": "/bin/sh",
+ "AGENT_ISO_AGENT": "kiro",
+ },
+ cwd=str(tmp_path),
+ capture_output=True,
+ text=True,
+ )
+ assert result.returncode == 127
+ assert "kiro-cli-iso: 'kiro-cli' not found" in result.stderr
diff --git a/tools/permission-audit/README.md b/tools/permission-audit/README.md
index fd53f3f9..a5dca34d 100644
--- a/tools/permission-audit/README.md
+++ b/tools/permission-audit/README.md
@@ -26,7 +26,7 @@
**Capability:** substrate:sandbox
-**Harness:** Claude Code, OpenCode
+**Harness:** Claude Code, OpenCode, Kiro
Audit + atomically edit Claude Code's `permissions.allow[]` entries
in `<repo>/.claude/settings.json` and `<repo>/.claude/settings.local.json`.
@@ -60,6 +60,29 @@ uv run --project tools/permission-audit permission-audit audit-opencode opencode
The Claude-only `apply` subcommand (atomic allow-list edits) has no
OpenCode counterpart yet; `audit-opencode` is read-only.
+**Kiro.** `audit-kiro` reads a Kiro CLI agent config
+(`.kiro/agents/<name>.json`). Kiro models permissions as an
+allowlist/denylist rather than a per-command decision map
+([Kiro shell tool](https://kiro.dev/docs/cli/reference/built-in-tools#execute-shell-commands)):
+`allowedCommands` / `deniedCommands` are `\A..\z`-anchored regex,
+deny-before-allow, with `denyByDefault` and `autoAllowReadonly`; and the
+shell tool in `allowedTools` auto-approves everything (overriding
+`toolsSettings`). Same intent, reported in JSON:
+
+- `shell-allow-all` — the shell tool (`shell` / `execute_bash` / `*` /
+ `@builtin`) is in `allowedTools` (every shell command auto-approved);
+- `dangerous-allow` — an `allowedCommands` regex auto-approves a dangerous
+ family (unblocked by `deniedCommands`);
+- `exfil-allow` — `aws` / `web_fetch` is blanket-allowed in `allowedTools`,
+ or `web_fetch.trusted` is a blanket `.*`.
+
+```bash
+uv run --project tools/permission-audit permission-audit audit-kiro .kiro/agents/<name>.json
+```
+
+Like `audit-opencode`, `audit-kiro` is read-only (no `apply`), and a
+*missing* shell policy is not flagged — Kiro's default is to prompt.
+
## Prerequisites
- **Runtime:** Python 3.11+ run via `uv` (`uv sync` / `uv run`); the
diff --git a/tools/permission-audit/src/permission_audit/cli.py b/tools/permission-audit/src/permission_audit/cli.py
index 8bb35ceb..c7ad3676 100644
--- a/tools/permission-audit/src/permission_audit/cli.py
+++ b/tools/permission-audit/src/permission_audit/cli.py
@@ -40,6 +40,7 @@ from permission_audit.audit import (
audit_settings,
)
from permission_audit.edit import apply_changes
+from permission_audit.kiro import audit_kiro
from permission_audit.opencode import audit_opencode
@@ -136,6 +137,22 @@ def _cmd_audit_opencode(args: argparse.Namespace) -> int:
return 1 if result.forbidden else 0
+def _cmd_audit_kiro(args: argparse.Namespace) -> int:
+ config_path = Path(args.config_path).resolve()
+ config = _read_opencode_config(config_path)
+ result = audit_kiro(config)
+
+ output = {
+ "config_path": str(config_path),
+ "file_exists": config_path.exists(),
+ "harness": "kiro",
+ "forbidden": [asdict(f) for f in result.forbidden],
+ }
+ json.dump(output, sys.stdout, indent=2)
+ sys.stdout.write("\n")
+ return 1 if result.forbidden else 0
+
+
def _cmd_list_known(args: argparse.Namespace) -> int:
output = {
"forbidden_patterns": sorted(FORBIDDEN_PATTERNS),
@@ -194,6 +211,13 @@ def _build_parser() -> argparse.ArgumentParser:
p_audit_oc.add_argument("config_path", help="Path to opencode.json.")
p_audit_oc.set_defaults(func=_cmd_audit_opencode)
+ p_audit_kiro = subparsers.add_parser(
+ "audit-kiro",
+ help="Audit a Kiro CLI agent-config (.kiro/agents/<name>.json) for over-permissioning.",
+ )
+ p_audit_kiro.add_argument("config_path", help="Path to a Kiro agent config JSON.")
+ p_audit_kiro.set_defaults(func=_cmd_audit_kiro)
+
p_known = subparsers.add_parser(
"list-known",
help="Print the canonical forbidden + recommended-by-family lists.",
diff --git a/tools/permission-audit/src/permission_audit/kiro.py b/tools/permission-audit/src/permission_audit/kiro.py
new file mode 100644
index 00000000..2c88bbd3
--- /dev/null
+++ b/tools/permission-audit/src/permission_audit/kiro.py
@@ -0,0 +1,170 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Audit a Kiro CLI agent-config for shell over-permissioning.
+
+Same intent as the Claude and OpenCode audits — flag config that
+**auto-approves dangerous shell execution** — over Kiro's allowlist/denylist
+model (`allowedTools` + `toolsSettings.shell`) rather than OpenCode's
+per-command `permission.bash` decision map.
+
+Kiro resolution (<https://kiro.dev/docs/cli/reference/built-in-tools>):
+the shell tool (`shell`/`execute_bash`/`execute_cmd`), `*`, or `@builtin` in
+`allowedTools` auto-approves *every* shell command (overriding
+`toolsSettings`); otherwise `deniedCommands` (regex, evaluated first) → deny,
+`allowedCommands` (regex) → allow, `denyByDefault` → deny, else prompt.
+`allowedCommands`/`deniedCommands` are `\\A..\\z`-anchored regex, matched here
+with :func:`re.fullmatch`.
+
+Pure data + classification — no I/O. The CLI layer reads the file and renders.
+"""
+
+from __future__ import annotations
+
+import re
+from dataclasses import dataclass, field
+
+from permission_audit.opencode import DANGEROUS_COMMANDS
+
+SHELL_TOOL_NAMES = frozenset({"shell", "execute_bash", "execute_cmd"})
+WILDCARD_TOOL_NAMES = frozenset({"*", "@builtin"})
+EXFIL_TOOLS: tuple[tuple[frozenset[str], str], ...] = (
+ (frozenset({"web_fetch", "webfetch"}), "auto-approves fetching arbitrary URLs (exfiltration)"),
+ (frozenset({"aws", "use_aws"}), "auto-approves AWS CLI calls (cloud credentials)"),
+)
+_BLANKET_TRUSTED = frozenset({".*", ".+", "^.*$", "(.*)"})
+
+
+@dataclass(frozen=True)
+class KiroFinding:
+ """A single Kiro permission finding.
+
+ `kind` is one of ``shell-allow-all`` (the shell tool is auto-approved via
+ `allowedTools`), ``dangerous-allow`` (an `allowedCommands` rule
+ auto-approves a dangerous family), or ``exfil-allow`` (a
+ credential/exfiltration surface is auto-approved).
+ """
+
+ severity: str # always "forbidden" today
+ kind: str
+ detail: str
+ json_pointer: str
+
+
+@dataclass
+class KiroAuditResult:
+ forbidden: list[KiroFinding] = field(default_factory=list)
+
+ @property
+ def has_findings(self) -> bool:
+ return bool(self.forbidden)
+
+
+def _shell_blanket_tool(allowed_tools: list) -> str | None:
+ for t in allowed_tools:
+ if isinstance(t, str) and (t in SHELL_TOOL_NAMES or t in WILDCARD_TOOL_NAMES):
+ return t
+ return None
+
+
+def _regex_fullmatch_any(patterns: object, command: str) -> str | None:
+ if not isinstance(patterns, list):
+ return None
+ for pat in patterns:
+ if not isinstance(pat, str):
+ continue
+ try:
+ if re.fullmatch(pat, command):
+ return pat
+ except re.error:
+ continue
+ return None
+
+
+def effective_shell_decision(shell_cfg: dict, command: str) -> tuple[str, str | None]:
+ """Resolve `command` to ``(decision, matched_pattern)`` — deny before allow."""
+ denied = _regex_fullmatch_any(shell_cfg.get("deniedCommands"), command)
+ if denied is not None:
+ return "deny", denied
+ allowed = _regex_fullmatch_any(shell_cfg.get("allowedCommands"), command)
+ if allowed is not None:
+ return "allow", allowed
+ if shell_cfg.get("denyByDefault"):
+ return "deny", None
+ return "ask", None
+
+
+def audit_kiro(config: dict) -> KiroAuditResult:
+ """Classify a Kiro agent-config dict for dangerous auto-approval."""
+ result = KiroAuditResult()
+
+ allowed_tools = config.get("allowedTools") or []
+ if not isinstance(allowed_tools, list):
+ allowed_tools = []
+ tools_settings = config.get("toolsSettings")
+ tools_settings = tools_settings if isinstance(tools_settings, dict) else {}
+ shell_cfg = tools_settings.get("shell")
+ shell_cfg = shell_cfg if isinstance(shell_cfg, dict) else {}
+
+ blanket = _shell_blanket_tool(allowed_tools)
+ if blanket is not None:
+ result.forbidden.append(
+ KiroFinding(
+ "forbidden",
+ "shell-allow-all",
+ f"the shell tool is auto-approved via allowedTools ({blanket!r}) — every shell "
+ "command runs with no gate, overriding toolsSettings. Gate it with "
+ "toolsSettings.shell (denyByDefault + a scoped allowedCommands allowlist).",
+ ".allowedTools",
+ )
+ )
+ else:
+ seen: set[str] = set()
+ for command, label in DANGEROUS_COMMANDS:
+ decision, matched = effective_shell_decision(shell_cfg, command)
+ if decision == "allow" and matched is not None and matched not in seen:
+ seen.add(matched)
+ result.forbidden.append(
+ KiroFinding(
+ "forbidden",
+ "dangerous-allow",
+ f"{label} is auto-approved by allowedCommands rule {matched!r}.",
+ f".toolsSettings.shell.allowedCommands[{matched!r}]",
+ )
+ )
+
+ for names, why in EXFIL_TOOLS:
+ hit = next((t for t in allowed_tools if isinstance(t, str) and t in names), None)
+ if hit is not None:
+ result.forbidden.append(
+ KiroFinding("forbidden", "exfil-allow", f"{hit!r} {why}.", ".allowedTools")
+ )
+
+ web_fetch = tools_settings.get("web_fetch")
+ if isinstance(web_fetch, dict):
+ for pat in web_fetch.get("trusted") or []:
+ if isinstance(pat, str) and pat in _BLANKET_TRUSTED:
+ result.forbidden.append(
+ KiroFinding(
+ "forbidden",
+ "exfil-allow",
+ f"web_fetch.trusted blanket-allows every URL ({pat!r}).",
+ ".toolsSettings.web_fetch.trusted",
+ )
+ )
+
+ return result
diff --git a/tools/permission-audit/tests/test_kiro.py b/tools/permission-audit/tests/test_kiro.py
new file mode 100644
index 00000000..4792b309
--- /dev/null
+++ b/tools/permission-audit/tests/test_kiro.py
@@ -0,0 +1,114 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Tests for the Kiro CLI permission audit."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+from permission_audit.cli import main
+from permission_audit.kiro import audit_kiro, effective_shell_decision
+
+
+def _kinds(config: dict) -> list[str]:
+ return [f.kind for f in audit_kiro(config).forbidden]
+
+
+def test_empty_config_is_clean():
+ # Kiro's default prompts for shell — no config is safe.
+ assert not audit_kiro({}).has_findings
+
+
+def test_safe_denybydefault_allowlist_is_clean():
+ config = {"toolsSettings": {"shell": {"denyByDefault": True, "allowedCommands": ["git status", "ls .*"]}}}
+ assert not audit_kiro(config).has_findings
+
+
+def test_shell_in_allowed_tools_is_forbidden():
+ r = audit_kiro({"allowedTools": ["read", "shell"]})
+ assert [f.kind for f in r.forbidden] == ["shell-allow-all"]
+ assert r.forbidden[0].json_pointer == ".allowedTools"
+
+
+def test_execute_bash_alias_forbidden():
+ assert _kinds({"allowedTools": ["execute_bash"]}) == ["shell-allow-all"]
+
+
+def test_wildcard_and_builtin_forbidden():
+ assert _kinds({"allowedTools": ["*"]}) == ["shell-allow-all"]
+ assert _kinds({"allowedTools": ["@builtin"]}) == ["shell-allow-all"]
+
+
+def test_blanket_shell_reported_once_not_per_command():
+ r = audit_kiro({"allowedTools": ["shell"]})
+ assert len(r.forbidden) == 1
+
+
+def test_dangerous_allowed_command_flagged():
+ config = {"toolsSettings": {"shell": {"allowedCommands": ["sudo .*", "git status"]}}}
+ r = audit_kiro(config)
+ kinds = [f.kind for f in r.forbidden]
+ assert "dangerous-allow" in kinds
+ assert any("sudo" in f.detail for f in r.forbidden)
+
+
+def test_deny_before_allow_wins_no_flag():
+ config = {"toolsSettings": {"shell": {"allowedCommands": ["git .*"], "deniedCommands": ["git push .*"]}}}
+ r = audit_kiro(config)
+ assert not any("git push" in f.detail for f in r.forbidden)
+
+
+def test_aws_tool_forbidden():
+ assert _kinds({"allowedTools": ["aws"]}) == ["exfil-allow"]
+
+
+def test_web_fetch_tool_forbidden():
+ assert _kinds({"allowedTools": ["web_fetch"]}) == ["exfil-allow"]
+
+
+def test_web_fetch_blanket_trusted_forbidden():
+ r = audit_kiro({"toolsSettings": {"web_fetch": {"trusted": [".*"]}}})
+ assert [f.kind for f in r.forbidden] == ["exfil-allow"]
+
+
+def test_effective_shell_decision_deny_before_allow():
+ cfg = {"allowedCommands": ["git .*"], "deniedCommands": ["git push .*"]}
+ assert effective_shell_decision(cfg, "git status") == ("allow", "git .*")
+ assert effective_shell_decision(cfg, "git push origin main") == ("deny", "git push .*")
+ assert effective_shell_decision({}, "ls -la") == ("ask", None)
+ assert effective_shell_decision({"denyByDefault": True}, "ls -la") == ("deny", None)
+
+
+def test_cli_audit_kiro_denies_and_exits_1(tmp_path: Path, capsys):
+ cfg = tmp_path / "agent.json"
+ cfg.write_text(json.dumps({"allowedTools": ["shell"]}))
+ rc = main(["audit-kiro", str(cfg)])
+ assert rc == 1
+ out = json.loads(capsys.readouterr().out)
+ assert out["harness"] == "kiro"
+ assert out["forbidden"][0]["kind"] == "shell-allow-all"
+
+
+def test_cli_audit_kiro_clean_exits_0(tmp_path: Path, capsys):
+ cfg = tmp_path / "agent.json"
+ cfg.write_text(json.dumps({"toolsSettings": {"shell": {"denyByDefault": True}}}))
+ rc = main(["audit-kiro", str(cfg)])
+ assert rc == 0
+ out = json.loads(capsys.readouterr().out)
+ assert out["forbidden"] == []
diff --git a/tools/sandbox-lint/README.md b/tools/sandbox-lint/README.md
index 3e4a9e0b..f7d0add6 100644
--- a/tools/sandbox-lint/README.md
+++ b/tools/sandbox-lint/README.md
@@ -22,7 +22,7 @@
**Capability:** substrate:sandbox
-**Harness:** Claude Code, OpenCode
+**Harness:** Claude Code, OpenCode, Kiro
Lints `.claude/settings.json` against the shipped baseline at
`tools/sandbox-lint/expected.json`, and against the security
@@ -47,6 +47,25 @@ to `"allow"`, no rule may auto-approve a dangerous command family
uv run --project tools/sandbox-lint sandbox-lint --opencode opencode.json
```
+**Kiro.** `--kiro .kiro/agents/<name>.json` lints a Kiro CLI agent
+config. Kiro's shell posture is an allowlist/denylist rather than a
+per-command decision map
+([Kiro shell tool](https://kiro.dev/docs/cli/reference/built-in-tools#execute-shell-commands)):
+`allowedCommands` / `deniedCommands` are full-string-anchored regex,
+deny is evaluated before allow, `denyByDefault` is the native
+allowlist-safe posture, and Kiro's default (no config) is to prompt —
+so, unlike OpenCode, a *missing* policy is not flagged. The invariants:
+the shell tool (`shell` / `execute_bash` / `*` / `@builtin`) must not be
+in `allowedTools` (that auto-approves every shell command and overrides
+`toolsSettings`), no `allowedCommands` regex may auto-approve a dangerous
+family (and stay unblocked by `deniedCommands`), `aws` / `web_fetch` must
+not be blanket-allowed in `allowedTools`, and `web_fetch.trusted` must
+not be a blanket `.*`.
+
+```bash
+uv run --project tools/sandbox-lint sandbox-lint --kiro .kiro/agents/<name>.json
+```
+
## Prerequisites
- **Runtime:** Python 3.11+ run via `uv` (stdlib only, no third-party deps).
diff --git a/tools/sandbox-lint/src/sandbox_lint/__init__.py b/tools/sandbox-lint/src/sandbox_lint/__init__.py
index 611d8f2e..3a3ab3b8 100644
--- a/tools/sandbox-lint/src/sandbox_lint/__init__.py
+++ b/tools/sandbox-lint/src/sandbox_lint/__init__.py
@@ -279,6 +279,21 @@ def _lint_opencode(config_path: Path) -> int:
return 1
+def _lint_kiro(config_path: Path) -> int:
+ """Lint a Kiro CLI agent-config permission policy (invariants only)."""
+ from sandbox_lint.kiro import check_kiro_invariants
+
+ config = _load_json(config_path)
+ errors = check_kiro_invariants(config)
+ if not errors:
+ print(f"sandbox-lint: OK ({config_path} permission policy satisfies the Kiro invariants)")
+ return 0
+ print(f"sandbox-lint: Kiro permission-policy violations in {config_path}:", file=sys.stderr)
+ for e in errors:
+ print(f" - {e}", file=sys.stderr)
+ return 1
+
+
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
prog="sandbox-lint",
@@ -311,8 +326,21 @@ def main(argv: list[str] | None = None) -> int:
"config (invariants only — no baseline diff)."
),
)
+ parser.add_argument(
+ "--kiro",
+ type=Path,
+ default=None,
+ metavar="KIRO_AGENT_JSON",
+ help=(
+ "Lint a Kiro CLI agent-config (.kiro/agents/<name>.json) permission "
+ "policy against the Kiro security invariants instead of the Claude "
+ "Code sandbox config (invariants only — no baseline diff)."
+ ),
+ )
args = parser.parse_args(argv)
+ if args.kiro is not None:
+ return _lint_kiro(args.kiro)
if args.opencode is not None:
return _lint_opencode(args.opencode)
diff --git a/tools/sandbox-lint/src/sandbox_lint/kiro.py b/tools/sandbox-lint/src/sandbox_lint/kiro.py
new file mode 100644
index 00000000..239cd754
--- /dev/null
+++ b/tools/sandbox-lint/src/sandbox_lint/kiro.py
@@ -0,0 +1,148 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Security invariants for a Kiro CLI agent-config permission policy.
+
+Like the OpenCode lint, this asserts safety properties (no baseline diff) —
+an adopter's `.kiro/agents/<name>.json` is their own. The intent is identical
+to the Claude Code and OpenCode lints: the config must not **auto-approve
+dangerous shell execution or exfiltration surfaces**.
+
+Kiro's permission model differs in shape from OpenCode's per-command
+`permission.bash` decision map. Kiro resolves a shell command through
+(<https://kiro.dev/docs/cli/reference/built-in-tools#execute-shell-commands>):
+
+1. If the shell tool (`shell` / `execute_bash` / `execute_cmd`), `*`, or
+ `@builtin` is in `allowedTools`, **every** shell command is auto-approved
+ with no gate (and this overrides `toolsSettings`).
+2. Otherwise, per command: `deniedCommands` regex (evaluated first) → deny;
+ `allowedCommands` regex → allow; `autoAllowReadonly` + read-only → allow;
+ `denyByDefault` → deny; else → **ask** (prompt).
+
+`allowedCommands` / `deniedCommands` are full-string-anchored regex (`\\A..\\z`,
+no look-around), so a command is matched with :func:`re.fullmatch`.
+
+Unlike OpenCode, a **missing** shell policy is *not* flagged: Kiro's default is
+to prompt for `shell`, which is already safe. The danger is only in configs
+that add auto-approval. `autoAllowReadonly` is not probed — Kiro classifies
+read-only-ness itself and none of the dangerous families below are read-only.
+"""
+
+from __future__ import annotations
+
+import re
+from typing import Any
+
+from sandbox_lint.opencode import DANGEROUS_COMMANDS
+
+# Tool ids that, in `allowedTools`, auto-approve every shell command.
+SHELL_TOOL_NAMES = frozenset({"shell", "execute_bash", "execute_cmd"})
+WILDCARD_TOOL_NAMES = frozenset({"*", "@builtin"})
+
+# Non-shell tools whose presence in `allowedTools` is an exfiltration /
+# credential-access risk when auto-approved.
+EXFIL_TOOLS: tuple[tuple[frozenset[str], str], ...] = (
+ (frozenset({"web_fetch", "webfetch"}), "auto-approves fetching arbitrary URLs (exfiltration channel)"),
+ (frozenset({"aws", "use_aws"}), "auto-approves AWS CLI calls (cloud credentials)"),
+)
+
+
+def _shell_blanket_allowed(allowed_tools: list[Any]) -> bool:
+ return any(t in SHELL_TOOL_NAMES or t in WILDCARD_TOOL_NAMES for t in allowed_tools if isinstance(t, str))
+
+
+def _regex_fullmatch_any(patterns: Any, command: str) -> str | None:
+ """Return the first pattern that fully matches `command`, or None.
+
+ Mirrors Kiro's `\\A..\\z`-anchored regex semantics via ``re.fullmatch``.
+ Invalid patterns are skipped (Kiro would reject them at load time).
+ """
+ if not isinstance(patterns, list):
+ return None
+ for pat in patterns:
+ if not isinstance(pat, str):
+ continue
+ try:
+ if re.fullmatch(pat, command):
+ return pat
+ except re.error:
+ continue
+ return None
+
+
+def effective_shell_decision(shell_cfg: dict[str, Any], command: str) -> tuple[str, str | None]:
+ """Resolve `command` to (decision, matched_pattern) — deny before allow."""
+ denied = _regex_fullmatch_any(shell_cfg.get("deniedCommands"), command)
+ if denied is not None:
+ return "deny", denied
+ allowed = _regex_fullmatch_any(shell_cfg.get("allowedCommands"), command)
+ if allowed is not None:
+ return "allow", allowed
+ if shell_cfg.get("denyByDefault"):
+ return "deny", None
+ return "ask", None
+
+
+def check_kiro_invariants(config: dict[str, Any]) -> list[str]:
+ """Return a list of invariant violations; empty means the policy is safe."""
+ errors: list[str] = []
+
+ allowed_tools = config.get("allowedTools") or []
+ if not isinstance(allowed_tools, list):
+ allowed_tools = []
+ tools_settings = config.get("toolsSettings")
+ tools_settings = tools_settings if isinstance(tools_settings, dict) else {}
+ shell_cfg = tools_settings.get("shell")
+ shell_cfg = shell_cfg if isinstance(shell_cfg, dict) else {}
+
+ # #2 — blanket auto-approve of the shell tool via allowedTools.
+ if _shell_blanket_allowed(allowed_tools):
+ errors.append(
+ 'allowedTools: must not auto-approve the shell tool (contains "shell"/'
+ '"execute_bash"/"*"/"@builtin") — every shell command runs with no gate, '
+ "and this overrides toolsSettings.shell. Remove it and gate shell via "
+ "toolsSettings.shell (denyByDefault + a scoped allowedCommands allowlist)."
+ )
+ else:
+ # #4 — specific allowedCommands rules that auto-approve a dangerous family.
+ seen: set[str] = set()
+ for command, label in DANGEROUS_COMMANDS:
+ decision, matched = effective_shell_decision(shell_cfg, command)
+ if decision == "allow" and matched is not None and matched not in seen:
+ seen.add(matched)
+ errors.append(
+ f"toolsSettings.shell.allowedCommands: must not auto-approve {label} "
+ f"(rule {matched!r} matches and is not denied)"
+ )
+
+ # #5 — exfil / credential tools blanket-allowed via allowedTools.
+ for names, why in EXFIL_TOOLS:
+ hit = next((t for t in allowed_tools if isinstance(t, str) and t in names), None)
+ if hit is not None:
+ errors.append(f"allowedTools: must not contain {hit!r} — it {why}; gate it instead")
+
+ # web_fetch.trusted must not blanket-allow every URL.
+ web_fetch = tools_settings.get("web_fetch")
+ if isinstance(web_fetch, dict):
+ for pat in web_fetch.get("trusted") or []:
+ if isinstance(pat, str) and pat in (".*", ".+", "^.*$", "(.*)"):
+ errors.append(
+ f"toolsSettings.web_fetch.trusted: must not be a blanket {pat!r} — "
+ "it auto-allows fetching any URL (exfiltration channel)"
+ )
+
+ return errors
diff --git a/tools/sandbox-lint/tests/test_kiro.py b/tools/sandbox-lint/tests/test_kiro.py
new file mode 100644
index 00000000..c46069d4
--- /dev/null
+++ b/tools/sandbox-lint/tests/test_kiro.py
@@ -0,0 +1,120 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Tests for the Kiro CLI permission-policy invariants of sandbox-lint."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+import pytest
+
+from sandbox_lint import main
+from sandbox_lint.kiro import check_kiro_invariants
+
+
+def test_empty_config_ok() -> None:
+ # Kiro's default is to prompt for shell — a config with no shell policy is
+ # safe, unlike OpenCode where a missing permission is flagged.
+ assert check_kiro_invariants({}) == []
+
+
+def test_safe_denybydefault_allowlist_ok() -> None:
+ config = {
+ "toolsSettings": {
+ "shell": {
+ "denyByDefault": True,
+ "allowedCommands": ["git status", "git fetch", "ls .*"],
+ }
+ }
+ }
+ assert check_kiro_invariants(config) == []
+
+
+def test_shell_in_allowed_tools_flagged() -> None:
+ errs = check_kiro_invariants({"allowedTools": ["read", "shell"]})
+ assert len(errs) == 1
+ assert "allowedTools" in errs[0] and "shell" in errs[0]
+
+
+def test_execute_bash_alias_in_allowed_tools_flagged() -> None:
+ assert check_kiro_invariants({"allowedTools": ["execute_bash"]})
+
+
+def test_wildcard_allowed_tools_flagged() -> None:
+ assert check_kiro_invariants({"allowedTools": ["*"]})
+ assert check_kiro_invariants({"allowedTools": ["@builtin"]})
+
+
+def test_dangerous_allowed_command_flagged() -> None:
+ config = {"toolsSettings": {"shell": {"allowedCommands": ["sudo .*", "git status"]}}}
+ errs = check_kiro_invariants(config)
+ assert any("sudo" in e for e in errs)
+
+
+def test_curl_allowed_command_flagged() -> None:
+ config = {"toolsSettings": {"shell": {"allowedCommands": ["curl .*"]}}}
+ errs = check_kiro_invariants(config)
+ assert any("curl" in e for e in errs)
+
+
+def test_deny_before_allow_wins_no_flag() -> None:
+ # allowedCommands opens git broadly, but deniedCommands (evaluated first)
+ # covers the only dangerous git sample — so no git finding.
+ config = {
+ "toolsSettings": {
+ "shell": {
+ "allowedCommands": ["git .*"],
+ "deniedCommands": ["git push .*"],
+ }
+ }
+ }
+ errs = check_kiro_invariants(config)
+ assert not any("git push" in e for e in errs)
+
+
+def test_aws_in_allowed_tools_flagged() -> None:
+ errs = check_kiro_invariants({"allowedTools": ["aws"]})
+ assert any("aws" in e for e in errs)
+
+
+def test_web_fetch_in_allowed_tools_flagged() -> None:
+ errs = check_kiro_invariants({"allowedTools": ["web_fetch"]})
+ assert any("web_fetch" in e for e in errs)
+
+
+def test_web_fetch_blanket_trusted_flagged() -> None:
+ config = {"toolsSettings": {"web_fetch": {"trusted": [".*"]}}}
+ errs = check_kiro_invariants(config)
+ assert any("web_fetch.trusted" in e for e in errs)
+
+
+def test_cli_kiro_clean(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
+ cfg = tmp_path / "agent.json"
+ cfg.write_text(json.dumps({"toolsSettings": {"shell": {"denyByDefault": True}}}))
+ rc = main(["--kiro", str(cfg)])
+ assert rc == 0
+ assert "OK" in capsys.readouterr().out
+
+
+def test_cli_kiro_violation(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
+ cfg = tmp_path / "agent.json"
+ cfg.write_text(json.dumps({"allowedTools": ["shell"]}))
+ rc = main(["--kiro", str(cfg)])
+ assert rc == 1
+ assert "violations" in capsys.readouterr().err
diff --git a/tools/spec-loop/README.md b/tools/spec-loop/README.md
index ae18f24d..12ce9c3a 100644
--- a/tools/spec-loop/README.md
+++ b/tools/spec-loop/README.md
@@ -5,7 +5,7 @@
**Capability:** substrate:framework-dev
-**Harness:** Claude Code, Codex, Cursor, Gemini CLI, OpenCode
+**Harness:** Claude Code, Codex, Cursor, Gemini CLI, OpenCode, Kiro
A spec-driven build loop for this framework, in the general
[Ralph](https://ghuntley.com/ralph/) style (run a fresh agent context
diff --git a/tools/spec-loop/lib.sh b/tools/spec-loop/lib.sh
index dfd2bec2..eaa95299 100644
--- a/tools/spec-loop/lib.sh
+++ b/tools/spec-loop/lib.sh
@@ -110,6 +110,12 @@ spec_loop_launch_agent() {
--yolo \
${model_args[@]+"${model_args[@]}"} \
--prompt "$(cat "$prompt_file")" &
+ elif [ "$harness" = "kiro" ]; then
+ # Kiro CLI headless: positional prompt to `kiro-cli chat --no-interactive`.
+ # The model is selected by the chosen agent config (.kiro/agents), not a
+ # per-invocation flag, and Kiro has no stream-json output mode here.
+ "$agent" chat --no-interactive \
+ "$(cat "$prompt_file")" &
else
local verbose_args=()
[ "$output_format" = "stream-json" ] && verbose_args=(--verbose)
diff --git a/tools/spec-loop/loop.sh b/tools/spec-loop/loop.sh
index 8b763497..fb5da88a 100755
--- a/tools/spec-loop/loop.sh
+++ b/tools/spec-loop/loop.sh
@@ -106,6 +106,7 @@ case "${SPEC_LOOP_HARNESS:-$(basename "$AGENT")}" in
*cursor*) HARNESS=cursor ;;
*gemini*) HARNESS=gemini ;;
*opencode*) HARNESS=opencode ;;
+ *kiro*) HARNESS=kiro ;;
*) HARNESS=claude ;;
esac
if [ -n "${SPEC_LOOP_MODEL:-}" ]; then
diff --git a/tools/spec-loop/specs/spec-loop-runner.md b/tools/spec-loop/specs/spec-loop-runner.md
index 1efdbd43..197deb33 100644
--- a/tools/spec-loop/specs/spec-loop-runner.md
+++ b/tools/spec-loop/specs/spec-loop-runner.md
@@ -20,7 +20,7 @@ acceptance:
- Open PRs and local work-item branches are passed into plan/build prompts
as in-flight work so the loop does not duplicate already-built items.
- The headless-agent harness contract is explicit for Claude Code, Codex,
- Cursor, Gemini CLI, and OpenCode.
+ Cursor, Gemini CLI, OpenCode, and Kiro.
- The update marker `tools/spec-loop/.last-sync` is owned by the runner,
not by the prompt.
---
@@ -109,6 +109,7 @@ sandbox, and stop without pushing or opening a PR.
| Cursor | positional prompt to `cursor agent --print` or `cursor-agent --print` | `--workspace "$ROOT"` | `--force --trust` | `--model` | `--output-format` | external sandbox and Cursor policy |
| Gemini CLI | `--prompt "<prompt>"` | launched from repo root | `--yolo` | `--model` | default CLI output | external sandbox and Gemini policy |
| OpenCode | positional prompt to `opencode run` | launched from repo root | `--auto` | `--model` | `--format json` for stream JSON | external sandbox and OpenCode policy |
+| Kiro | positional prompt to `kiro-cli chat` | launched from repo root | `--no-interactive` | agent config (`.kiro/agents`) | plain text only | external sandbox and the agent-guard `--kiro` hook |
`SPEC_LOOP_AGENT` chooses the CLI. `SPEC_LOOP_HARNESS` chooses the
invocation convention and defaults from the agent basename. Adding a new
@@ -135,7 +136,7 @@ updating `loop.sh` in the same change.
branch context.
4. Update prompts receive incremental-scope guidance from `.last-sync`
when present, but the runner remains the only writer of `.last-sync`.
-5. Claude Code, Codex, Cursor, Gemini CLI, and OpenCode are documented in
+5. Claude Code, Codex, Cursor, Gemini CLI, OpenCode, and Kiro are documented in
the headless harness matrix and implemented in `loop.sh`.
6. Operator docs and prompts agree on branch naming: build uses bare
`<slug>` branches; update uses `sync-specs-<timestamp>` branches.
diff --git a/tools/spec-loop/tests/test_runner_fixtures.sh b/tools/spec-loop/tests/test_runner_fixtures.sh
index 20dda4e7..a647500a 100755
--- a/tools/spec-loop/tests/test_runner_fixtures.sh
+++ b/tools/spec-loop/tests/test_runner_fixtures.sh
@@ -114,6 +114,14 @@ test_harness_command_construction() {
assert_contains "$TMPDIR_TEST/codex.log" "<--json>"
assert_contains "$TMPDIR_TEST/codex.log" "<->"
assert_contains "$TMPDIR_TEST/codex.log" "stdin:PROMPT BODY"
+
+ make_fake_agent "$TMPDIR_TEST/kiro-cli" "$TMPDIR_TEST/kiro.log"
+ spec_loop_launch_agent kiro "$TMPDIR_TEST/kiro-cli" /repo "$TMPDIR_TEST/prompt.md" "" text
+ wait "$SPEC_LOOP_AGENT_PID"
+ assert_contains "$TMPDIR_TEST/kiro.log" "<chat>"
+ assert_contains "$TMPDIR_TEST/kiro.log" "<--no-interactive>"
+ assert_contains "$TMPDIR_TEST/kiro.log" "<PROMPT BODY>"
+ assert_not_contains "$TMPDIR_TEST/kiro.log" "<--model>"
}
test_last_sync_marker_helpers() {