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() {