From 998a0acc1aeb61d1ce08fcf09492ee4196c6115d Mon Sep 17 00:00:00 2001
From: danielmeppiel <danielmeppiel@users.noreply.github.com>
Date: Thu, 11 Jun 2026 00:42:58 +0200
Subject: [PATCH 1/9] feat(targets): add Kiro IDE support

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 CHANGELOG.md                                  |   5 +
 docs/src/content/docs/concepts/glossary.md    |   2 +-
 docs/src/content/docs/concepts/lifecycle.md   |   2 +-
 .../docs/concepts/primitives-and-targets.md   |  24 +-
 docs/src/content/docs/concepts/what-is-apm.md |   2 +-
 .../docs/consumer/install-mcp-servers.md      |   3 +-
 .../content/docs/consumer/install-packages.md |   6 +-
 docs/src/content/docs/consumer/run-scripts.md |   2 +-
 .../docs/enterprise/policy-reference.md       |   2 +-
 docs/src/content/docs/enterprise/security.md  |   2 +-
 .../docs/getting-started/first-package.md     |   2 +-
 .../docs/integrations/ide-tool-integration.md |  16 +-
 .../author-primitives/hooks-and-commands.md   |   3 +-
 .../instructions-and-agents.md                |   6 +-
 .../docs/producer/author-primitives/skills.md |   5 +-
 docs/src/content/docs/producer/compile.md     |  11 +-
 .../src/content/docs/reference/cli/compile.md |   7 +-
 docs/src/content/docs/reference/cli/deps.md   |   2 +-
 docs/src/content/docs/reference/cli/init.md   |   2 +-
 .../src/content/docs/reference/cli/install.md |   6 +-
 .../src/content/docs/reference/cli/targets.md |   2 +
 .../content/docs/reference/cli/uninstall.md   |   2 +-
 docs/src/content/docs/reference/cli/update.md |   2 +-
 .../content/docs/reference/manifest-schema.md |   9 +-
 .../content/docs/reference/targets-matrix.md  |  21 +-
 .../compile-zero-output-warning.md            |   2 +-
 .../.apm/skills/apm-usage/commands.md         |   4 +-
 .../.apm/skills/apm-usage/dependencies.md     |   1 +
 .../skills/apm-usage/package-authoring.md     |  12 +-
 src/apm_cli/adapters/client/kiro.py           | 220 +++++++++++++
 src/apm_cli/compilation/agents_compiler.py    |   1 +
 src/apm_cli/core/apm_yml.py                   |   1 +
 src/apm_cli/core/target_detection.py          |  24 +-
 src/apm_cli/factory.py                        |   2 +
 src/apm_cli/integration/hook_integrator.py    |  40 ++-
 .../integration/instruction_integrator.py     |  52 +++
 .../integration/kiro_hook_integrator.py       | 308 ++++++++++++++++++
 src/apm_cli/integration/mcp_integrator.py     |  22 +-
 .../integration/mcp_integrator_install.py     | 132 ++++----
 src/apm_cli/integration/targets.py            |  34 +-
 src/apm_cli/policy/policy_checks.py           |   1 +
 tests/unit/core/test_scope.py                 |   5 +
 tests/unit/core/test_target_detection.py      |  23 ++
 tests/unit/core/test_target_resolution_v2.py  |   5 +
 .../integration/test_data_driven_dispatch.py  |   1 +
 tests/unit/integration/test_kiro_target.py    | 199 +++++++++++
 .../test_targets_registry_completeness.py     |   2 +
 tests/unit/test_kiro_mcp.py                   | 162 +++++++++
 48 files changed, 1268 insertions(+), 131 deletions(-)
 create mode 100644 src/apm_cli/adapters/client/kiro.py
 create mode 100644 src/apm_cli/integration/kiro_hook_integrator.py
 create mode 100644 tests/unit/integration/test_kiro_target.py
 create mode 100644 tests/unit/test_kiro_mcp.py

diff --git a/CHANGELOG.md b/CHANGELOG.md
index db5657d05..54dcafd50 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+### Added
+
+- Kiro IDE is now a first-class install target: APM deploys steering, skills,
+  hooks, and MCP config to the documented `.kiro/` layout. (closes #702)
+
 ## [0.19.0] - 2026-06-09
 
 ### Added
diff --git a/docs/src/content/docs/concepts/glossary.md b/docs/src/content/docs/concepts/glossary.md
index a59d4f808..b3ad9c89b 100644
--- a/docs/src/content/docs/concepts/glossary.md
+++ b/docs/src/content/docs/concepts/glossary.md
@@ -252,7 +252,7 @@ Source: `src/apm_cli/models/apm_package.py`,
 
 The `targets:` field in `apm.yml` (or legacy `target:`). Names which harnesses the package
 compiles for (`copilot`, `claude`, `cursor`, `codex`, `gemini`,
-`opencode`, `windsurf`, or `all`). Drives which integrator runs and
+`opencode`, `windsurf`, `kiro`, or `all`). Drives which integrator runs and
 which directories receive output during `apm compile`.
 
 NOT the harness itself. Target is the declaration; the harness is the
diff --git a/docs/src/content/docs/concepts/lifecycle.md b/docs/src/content/docs/concepts/lifecycle.md
index 1f57644d1..506866094 100644
--- a/docs/src/content/docs/concepts/lifecycle.md
+++ b/docs/src/content/docs/concepts/lifecycle.md
@@ -30,7 +30,7 @@ Scaffolds a new APM project in the current directory.
 
 `apm init` writes an `apm.yml` manifest with sensible defaults for `name`, `author`, and `description`, plus empty dependency and script blocks. It records selected targets in `targets:`; author `.apm/` primitives yourself and run `apm install` or `apm compile` to create target output directories.
 
-Targets are picked in priority order. An explicit `--target copilot,claude` flag wins. Otherwise an interactive checklist runs. Otherwise APM scans the working tree for signal directories (`.github/`, `.claude/`, `.cursor/`, `.opencode/`, `.codex/`, `.gemini/`, `.windsurf/`) and pre-checks every harness it finds. With `-y` and no flag, all detected harnesses are written into `apm.yml`. See [primitives and targets](/apm/concepts/primitives-and-targets/) for what each target actually receives.
+Targets are picked in priority order. An explicit `--target copilot,claude` flag wins. Otherwise an interactive checklist runs. Otherwise APM scans the working tree for signal directories (`.github/`, `.claude/`, `.cursor/`, `.opencode/`, `.codex/`, `.gemini/`, `.windsurf/`, `.kiro/`) and pre-checks every harness it finds. With `-y` and no flag, all detected harnesses are written into `apm.yml`. See [primitives and targets](/apm/concepts/primitives-and-targets/) for what each target actually receives.
 
 **Common surprises**
 
diff --git a/docs/src/content/docs/concepts/primitives-and-targets.md b/docs/src/content/docs/concepts/primitives-and-targets.md
index 06e2e9956..da9eba233 100644
--- a/docs/src/content/docs/concepts/primitives-and-targets.md
+++ b/docs/src/content/docs/concepts/primitives-and-targets.md
@@ -81,6 +81,7 @@ Each target is identified by a slug used in `apm.yml`'s `targets:` field and on
 | `gemini` | `.gemini/` | gemini |
 | `opencode` | `.opencode/` (project), `~/.config/opencode/` (user) | agents |
 | `windsurf` | `.windsurf/` (project), `~/.codeium/windsurf/` (user) | agents |
+| `kiro` | `.kiro/` (project and user) | agents |
 
 Notes per target:
 
@@ -91,6 +92,7 @@ Notes per target:
 - **gemini** -- Gemini CLI. Commands are TOML. Hooks merge into `.gemini/settings.json`. No native agents or instructions primitives -- both arrive via compiled context files.
 - **opencode** -- OpenCode. No hooks support.
 - **windsurf** -- Windsurf / Cascade. No native agents primitive -- Cascade auto-invokes any `SKILL.md` by its `description:` frontmatter, so personas ship as skills. Workflows are the harness's name for commands.
+- **kiro** -- Kiro IDE. Instructions become steering files, skills stay as `SKILL.md` folders, hooks are individual JSON files, and MCP lands in `.kiro/settings/mcp.json`.
 
 ## The compatibility matrix
 
@@ -101,16 +103,16 @@ Rows are primitives, columns are harnesses. Cell legend:
 - **unsupported** -- APM does not deliver this primitive to this harness.
 - **gated** -- delivered behind an explicit declaration or trust flag.
 
-| Primitive | Copilot | Claude | Cursor | Codex | Gemini | OpenCode | Windsurf |
-|---|---|---|---|---|---|---|---|
-| instructions | native | native | native | compiled | compiled | compiled | native |
-| prompts | native | compiled | compiled | unsupported | compiled | compiled | compiled |
-| agents | native | native | compiled | compiled | unsupported | native | unsupported |
-| skills | native | native | native | native | native | native | native |
-| hooks | native | native | native | native | native | unsupported | native |
-| commands | unsupported | native | compiled | unsupported | compiled | compiled | compiled |
-| plugins | compiled | compiled | compiled | compiled | compiled | compiled | compiled |
-| MCP servers | native | native | native | native | native | native | native |
+| Primitive | Copilot | Claude | Cursor | Codex | Gemini | OpenCode | Windsurf | Kiro |
+|---|---|---|---|---|---|---|---|---|
+| instructions | native | native | native | compiled | compiled | compiled | native | native |
+| prompts | native | compiled | compiled | unsupported | compiled | compiled | compiled | unsupported |
+| agents | native | native | compiled | compiled | unsupported | native | unsupported | unsupported |
+| skills | native | native | native | native | native | native | native | native |
+| hooks | native | native | native | native | native | unsupported | native | native |
+| commands | unsupported | native | compiled | unsupported | compiled | compiled | compiled | unsupported |
+| plugins | compiled | compiled | compiled | compiled | compiled | compiled | compiled | compiled |
+| MCP servers | native | native | native | native | native | native | native | native |
 
 How to read a cell:
 
@@ -158,7 +160,7 @@ Full pattern, the three pack-time gotchas, and verification steps: [Dev-only pri
 
 1. Explicit `--target <slug>` flag, when passed.
 2. The `targets:` field in `apm.yml`, when present.
-3. Auto-detection: any harness whose root directory (`.github/`, `.claude/`, `.cursor/`, `.codex/`, `.gemini/`, `.opencode/`, `.windsurf/`) already exists in the workspace is selected.
+3. Auto-detection: any harness whose root directory (`.github/`, `.claude/`, `.cursor/`, `.codex/`, `.gemini/`, `.opencode/`, `.windsurf/`, `.kiro/`) already exists in the workspace is selected.
 4. Fallback: `minimal` -- APM writes `AGENTS.md` only and skips folder
    integration. Create one of the harness folders above (or set
    `targets:` explicitly) for full integration.
diff --git a/docs/src/content/docs/concepts/what-is-apm.md b/docs/src/content/docs/concepts/what-is-apm.md
index 96d55a533..704236c54 100644
--- a/docs/src/content/docs/concepts/what-is-apm.md
+++ b/docs/src/content/docs/concepts/what-is-apm.md
@@ -15,7 +15,7 @@ APM borrows the manifest-plus-lockfile shape from `npm`, `pip`, and `cargo` and
 
 `apm.yml` is the manifest. It lists agentic dependencies (skills, prompts, agents, plugins, full APM packages) and MCP servers. `apm.lock.yaml` is the lockfile. It pins every resolved package to an exact source ref and content hash, so two developers running `apm install` against the same lockfile get byte-identical context. Source authoring lives in `.apm/` inside your repo.
 
-The compiled output lives in the directories each harness already reads: `.github/` for Copilot, `.claude/` for Claude Code, `.cursor/` for Cursor, `.codex/` and `AGENTS.md` for Codex, `.gemini/` for Gemini, `.opencode/` for OpenCode, `.windsurf/` for Windsurf. APM does not invent a runtime format. It writes the files each tool already understands and stays out of the way at agent runtime.
+The compiled output lives in the directories each harness already reads: `.github/` for Copilot, `.claude/` for Claude Code, `.cursor/` for Cursor, `.codex/` and `AGENTS.md` for Codex, `.gemini/` for Gemini, `.opencode/` for OpenCode, `.windsurf/` for Windsurf, and `.kiro/` for Kiro. APM does not invent a runtime format. It writes the files each tool already understands and stays out of the way at agent runtime.
 
 ## What APM manages
 
diff --git a/docs/src/content/docs/consumer/install-mcp-servers.md b/docs/src/content/docs/consumer/install-mcp-servers.md
index 10d8637a0..548787eae 100644
--- a/docs/src/content/docs/consumer/install-mcp-servers.md
+++ b/docs/src/content/docs/consumer/install-mcp-servers.md
@@ -85,6 +85,7 @@ writes a runtime-specific MCP config file. The schemas differ; the
 | Gemini CLI | `.gemini/settings.json` (project, only if `.gemini/` exists) or `~/.gemini/settings.json` (`-g`) | both | JSON `mcpServers` |
 | OpenCode | `opencode.json` | project (only if `.opencode/` exists) | JSON `mcp` |
 | Windsurf | `~/.codeium/windsurf/mcp_config.json` | global | JSON `mcpServers` |
+| Kiro IDE | `.kiro/settings/mcp.json` (project, only if `.kiro/` exists) or `~/.kiro/settings/mcp.json` (`-g`) | both | JSON `mcpServers` |
 
 ## How `targets:` gates which configs get written
 
@@ -122,7 +123,7 @@ declare one in `apm.yml`. (#1335)
 `apm install -g --mcp NAME` is a deliberate carve-out: it routes the
 write to each runtime's user-scope MCP config (Copilot CLI to
 `~/.copilot/mcp-config.json`, Codex CLI to `~/.codex/config.toml`,
-Gemini CLI to `~/.gemini/settings.json`) and does not consult the
+Gemini CLI to `~/.gemini/settings.json`, Kiro to `~/.kiro/settings/mcp.json`) and does not consult the
 project-scope `targets:` whitelist -- user-scope writes are by
 definition not project-bound. Workspace-only runtimes (VS Code,
 Cursor, OpenCode) are skipped at user scope.
diff --git a/docs/src/content/docs/consumer/install-packages.md b/docs/src/content/docs/consumer/install-packages.md
index be5265ab0..e87dd8963 100644
--- a/docs/src/content/docs/consumer/install-packages.md
+++ b/docs/src/content/docs/consumer/install-packages.md
@@ -81,7 +81,7 @@ The pipeline is deterministic. Each phase must pass before the next runs.
    `--force`.
 4. **Integrate.** Write primitives into each target harness's native
    directory (`.github/`, `.claude/`, `.cursor/`, `.opencode/`,
-   `.codex/`, `.gemini/`, `.windsurf/`) and the cross-tool
+   `.codex/`, `.gemini/`, `.windsurf/`, `.kiro/`) and the cross-tool
    `.agents/skills/` directory.
 5. **Lockfile.** Write `apm.lock.yaml` with pinned versions, content
    hashes, and the resolved dependency set.
@@ -105,7 +105,7 @@ them. Detection priority:
 1. `--target <slug>` flag (highest).
 2. The `targets:` field in `apm.yml`.
 3. Auto-detect: any harness directory (`.github/`, `.claude/`,
-   `.cursor/`, `.opencode/`, `.codex/`, `.gemini/`, `.windsurf/`)
+   `.cursor/`, `.opencode/`, `.codex/`, `.gemini/`, `.windsurf/`, `.kiro/`)
    that already exists in the workspace.
 4. Fallback: minimal output to `AGENTS.md` only.
 
@@ -122,7 +122,7 @@ targets:
 For the full reach map of which primitive lands where on each
 harness, see [Primitives and targets](../../concepts/primitives-and-targets/).
 
-Rule sync to Cursor (`.cursor/rules/`), Claude Code (`.claude/rules/`), and Windsurf (`.windsurf/rules/`) is automatic and idempotent -- re-running `apm install` adopts unchanged rules without rewriting them.
+Rule sync to Cursor (`.cursor/rules/`), Claude Code (`.claude/rules/`), Windsurf (`.windsurf/rules/`), and Kiro (`.kiro/steering/`) is automatic and idempotent -- re-running `apm install` adopts unchanged rules without rewriting them.
 
 ## Transitive dependencies and the lockfile
 
diff --git a/docs/src/content/docs/consumer/run-scripts.md b/docs/src/content/docs/consumer/run-scripts.md
index 3ae07c55b..f911830be 100644
--- a/docs/src/content/docs/consumer/run-scripts.md
+++ b/docs/src/content/docs/consumer/run-scripts.md
@@ -35,7 +35,7 @@ scripts:
 
 Each value is a literal shell command. The canonical pattern is shelling
 out to a runtime CLI -- `copilot`, `claude`, `codex`, `cursor-agent`,
-`gemini`, `opencode`, `windsurf`, or `llm` -- with a prompt file argument.
+`gemini`, `opencode`, `windsurf`, `kiro`, or `llm` -- with a prompt file argument.
 APM does not bundle these runtimes; you install them yourself and APM
 invokes whichever the script names.
 
diff --git a/docs/src/content/docs/enterprise/policy-reference.md b/docs/src/content/docs/enterprise/policy-reference.md
index 900c09473..9105a5cd1 100644
--- a/docs/src/content/docs/enterprise/policy-reference.md
+++ b/docs/src/content/docs/enterprise/policy-reference.md
@@ -40,7 +40,7 @@ mcp:
 
 compilation:
   target:
-    allow: []                   # copilot | claude | cursor | opencode | codex | gemini | vscode | windsurf | agent-skills | all
+    allow: []                   # copilot | claude | cursor | opencode | codex | gemini | vscode | windsurf | kiro | agent-skills | all
     enforce: null               # Enforce specific target (must be present in list)
   strategy:
     enforce: null               # distributed | single-file
diff --git a/docs/src/content/docs/enterprise/security.md b/docs/src/content/docs/enterprise/security.md
index 09daf46da..8f297a54e 100644
--- a/docs/src/content/docs/enterprise/security.md
+++ b/docs/src/content/docs/enterprise/security.md
@@ -208,7 +208,7 @@ APM deploys files only to controlled subdirectories within the project root.
 All deploy paths are validated before any file operation:
 
 1. **No `..` segments.** Any path containing `..` is rejected outright.
-2. **Allowed prefixes only.** Paths must start with an allowed target-integrator prefix (`.github/`, `.claude/`, `.cursor/`, `.opencode/`, `.codex/`, `.gemini/`, `.windsurf/`, `.agents/`). In addition, the local-bundle install path stages instructions for compile-only targets under `apm_modules/<slug>/.apm/instructions/` with its own containment check (the resolved path must remain within `apm_modules/`) and `<slug>` validation rejecting traversal sequences and characters outside `[A-Za-z0-9._-]`.
+2. **Allowed prefixes only.** Paths must start with an allowed target-integrator prefix (`.github/`, `.claude/`, `.cursor/`, `.opencode/`, `.codex/`, `.gemini/`, `.windsurf/`, `.kiro/`, `.agents/`). In addition, the local-bundle install path stages instructions for compile-only targets under `apm_modules/<slug>/.apm/instructions/` with its own containment check (the resolved path must remain within `apm_modules/`) and `<slug>` validation rejecting traversal sequences and characters outside `[A-Za-z0-9._-]`.
 3. **Resolution containment.** The fully resolved path must remain within the project root directory.
 
 A path must pass all three checks. Failure on any check prevents the file from being written.
diff --git a/docs/src/content/docs/getting-started/first-package.md b/docs/src/content/docs/getting-started/first-package.md
index 1d62e8169..dc79df7b5 100644
--- a/docs/src/content/docs/getting-started/first-package.md
+++ b/docs/src/content/docs/getting-started/first-package.md
@@ -209,7 +209,7 @@ team-skills/
 `apm install` resolves which harness directories to populate using a strict
 priority chain: `--target` flag > `apm.yml` `targets:` > auto-detect from
 filesystem signals (`.claude/`, `CLAUDE.md`, `.cursor/`, `.github/copilot-instructions.md`,
-`.codex/`, `.gemini/`, `GEMINI.md`, `.opencode/`, `.windsurf/`). The example layout
+`.codex/`, `.gemini/`, `GEMINI.md`, `.opencode/`, `.windsurf/`, `.kiro/`). The example layout
 above shows `.github/` because `.github/copilot-instructions.md` exists in the
 project; if you also have `.claude/`, `.cursor/`, `.opencode/`, or `.gemini/`, those
 directories get populated too. With no signal at all, `apm install` exits with
diff --git a/docs/src/content/docs/integrations/ide-tool-integration.md b/docs/src/content/docs/integrations/ide-tool-integration.md
index 13f419596..8110ae47c 100644
--- a/docs/src/content/docs/integrations/ide-tool-integration.md
+++ b/docs/src/content/docs/integrations/ide-tool-integration.md
@@ -22,6 +22,7 @@ The full slot-by-slot capability table lives in [Targets matrix](../reference/ta
 | Gemini CLI           | `.gemini/` or `GEMINI.md`            | Single-file or distributed             |
 | OpenCode             | `.opencode/`                         | Skills, MCP                            |
 | Windsurf             | `.windsurf/`                         | Rules + Skills + Workflows + MCP       |
+| Kiro                 | `.kiro/`                             | Steering + Skills + Hooks + MCP        |
 | JetBrains Copilot    | user-scope config dir (global)       | MCP only (user-scope path, `${env:VAR}` env substitution) |
 | Agent-Skills (cross) | `.agents/skills/`                    | Vendor-neutral skill sharing           |
 
@@ -59,7 +60,7 @@ Each primitive type maps to a target-specific slot:
 .apm/prompts/        ->   per target: prompt files / commands
 .apm/agents/         ->   per target: agent definitions (or skill conversion)
 .apm/skills/         ->   per target: skills directory (Claude, Codex, OpenCode, .agents)
-.apm/hooks/          ->   per target: lifecycle hooks (Claude only today)
+.apm/hooks/          ->   per target: lifecycle hooks / tool hooks
 mcp: in apm.yml      ->   per target: .mcp.json / settings.json / equivalent
 ```
 
@@ -107,6 +108,7 @@ MCP servers declared in `apm.yml` (under `dependencies.mcp:` or `devDependencies
 - `opencode.json` at the repo root when `.opencode/` exists (OpenCode)
 - `.gemini/settings.json` (Gemini)
 - `~/.codeium/windsurf/mcp_config.json` (Windsurf)
+- `.kiro/settings/mcp.json` and `~/.kiro/settings/mcp.json` (Kiro IDE)
 - OS-specific `github-copilot/intellij/mcp.json` (JetBrains Copilot -- uses
   `"servers"` key, user-scope global path):
   - `%LOCALAPPDATA%\github-copilot\intellij\mcp.json` (Windows)
@@ -115,6 +117,18 @@ MCP servers declared in `apm.yml` (under `dependencies.mcp:` or `devDependencies
 
 For server installation patterns, registry resolution, and trust model, see [MCP servers guide](../consumer/install-mcp-servers/) and [`apm mcp`](../reference/cli/mcp/).
 
+### Kiro IDE
+
+Kiro reads project configuration from `.kiro/`. APM maps instructions to
+`.kiro/steering/` and converts `applyTo:` scoping into Kiro steering
+frontmatter (`inclusion: fileMatch`); unscoped instructions become
+`inclusion: always`. Skills are copied verbatim to `.kiro/skills/`, hooks
+become one JSON file per hook in `.kiro/hooks/`, and MCP servers are written
+to `.kiro/settings/mcp.json` or `~/.kiro/settings/mcp.json` for `--global`.
+
+This target covers the documented Kiro IDE layout. Kiro CLI configuration
+differences are tracked separately.
+
 ### JetBrains (IntelliJ IDEA, PyCharm, GoLand, and others)
 
 GitHub Copilot for JetBrains reads MCP servers from a single user-scope
diff --git a/docs/src/content/docs/producer/author-primitives/hooks-and-commands.md b/docs/src/content/docs/producer/author-primitives/hooks-and-commands.md
index 24e586e5c..c052afaed 100644
--- a/docs/src/content/docs/producer/author-primitives/hooks-and-commands.md
+++ b/docs/src/content/docs/producer/author-primitives/hooks-and-commands.md
@@ -78,7 +78,7 @@ key is present but not a JSON object fails closed with a warning; a file
 that parses cleanly but contributes zero entries also logs a warning so
 authors notice empty merges during development.
 
-The `${PLUGIN_ROOT}`, `${CLAUDE_PLUGIN_ROOT}`, and `${CURSOR_PLUGIN_ROOT}`
+The `${PLUGIN_ROOT}`, `${CLAUDE_PLUGIN_ROOT}`, `${CURSOR_PLUGIN_ROOT}`, and `${KIRO_PLUGIN_ROOT}`
 tokens resolve to the installed package root and are rewritten per
 target. Plain `./script.sh` resolves relative to the hook file.
 
@@ -92,6 +92,7 @@ Supported targets and where the integrator writes:
 | gemini   | `.gemini/settings.json`               | merged               |
 | codex    | `.codex/hooks.json`                   | merged               |
 | windsurf | `.windsurf/hooks.json`                | merged               |
+| kiro     | `.kiro/hooks/<pkg>-<event>.json`    | one file per hook    |
 | opencode | -- not supported --                   | silently skipped     |
 
 Copilot hook files are namespaced with the source package name to avoid
diff --git a/docs/src/content/docs/producer/author-primitives/instructions-and-agents.md b/docs/src/content/docs/producer/author-primitives/instructions-and-agents.md
index 0b9b23d0b..a5dc2e87d 100644
--- a/docs/src/content/docs/producer/author-primitives/instructions-and-agents.md
+++ b/docs/src/content/docs/producer/author-primitives/instructions-and-agents.md
@@ -82,8 +82,8 @@ applyTo: "**/*.{css,scss},**/*.tsx"
 ```
 
 On Copilot the comma-list is preserved verbatim (Copilot splits it
-natively). On Claude, Cursor, and Windsurf the list is expanded to a
-YAML array under `paths:` / `globs:`.
+natively). On Claude, Cursor, Windsurf, and Kiro the list is expanded to a
+YAML array under `paths:` / `globs:` / `fileMatchPattern:`.
 
 ### Body conventions
 
@@ -103,6 +103,7 @@ YAML array under `paths:` / `globs:`.
 | claude | `.claude/rules/<name>.md` | `applyTo` -> `paths:` list (comma-lists expanded to YAML array) |
 | cursor | `.cursor/rules/<name>.mdc` | `applyTo` -> `globs:` (scalar for single glob, YAML array for comma-lists); description auto-derived if missing |
 | windsurf | `.windsurf/rules/<name>.md` | `applyTo` -> `trigger: glob` + `globs:` (scalar or YAML array); missing `applyTo` -> `trigger: always_on` |
+| kiro | `.kiro/steering/<name>.md` | `applyTo` -> `inclusion: fileMatch` + `fileMatchPattern:`; missing `applyTo` -> `inclusion: always` |
 | codex | folded into `AGENTS.md` | compile-only, no per-file deploy |
 | gemini | folded into `GEMINI.md` | compile-only, no per-file deploy |
 | opencode | folded into `AGENTS.md` | compile-only, no per-file deploy |
@@ -192,6 +193,7 @@ offending package and field so you can fix the source.
 | opencode | `.opencode/agents/<name>.md` | verbatim |
 | codex | `.codex/agents/<name>.toml` | YAML frontmatter -> TOML; body becomes `developer_instructions` |
 | windsurf | not deployed | Windsurf has no agents primitive -- author personas as skills (Cascade auto-invokes by description) |
+| kiro | not deployed | Kiro target v1 ships personas as skills, not `.agent.md` files |
 | gemini | not deployed | Gemini CLI has no agents primitive |
 
 :::caution[Migration]
diff --git a/docs/src/content/docs/producer/author-primitives/skills.md b/docs/src/content/docs/producer/author-primitives/skills.md
index 1f2484f31..f0d604037 100644
--- a/docs/src/content/docs/producer/author-primitives/skills.md
+++ b/docs/src/content/docs/producer/author-primitives/skills.md
@@ -100,6 +100,7 @@ in `references/`; keep `SKILL.md` to the always-relevant flow.
 |-------------------|----------------------------------------------|
 | `claude`          | `.claude/skills/<name>/SKILL.md`             |
 | `windsurf`        | `.windsurf/skills/<name>/SKILL.md`           |
+| `kiro`            | `.kiro/skills/<name>/SKILL.md`               |
 | `copilot`         | `.agents/skills/<name>/SKILL.md`             |
 | `cursor`          | `.agents/skills/<name>/SKILL.md`             |
 | `codex`           | `.agents/skills/<name>/SKILL.md`             |
@@ -109,8 +110,8 @@ in `references/`; keep `SKILL.md` to the always-relevant flow.
 
 Five harnesses converge on the cross-tool `.agents/skills/`
 directory. Claude keeps its harness-native path because Claude Code's
-default scan is `.claude/skills/`; Windsurf currently uses
-`.windsurf/skills/` for the same reason, though Cascade also
+default scan is `.claude/skills/`; Windsurf and Kiro currently use
+`.windsurf/skills/` and `.kiro/skills/` for the same reason, though Cascade also
 [discovers `.agents/skills/`](https://docs.windsurf.com/windsurf/cascade/skills#skill-scopes)
 natively for cross-agent compatibility (convergence tracked in
 [#1520](https://github.com/microsoft/apm/issues/1520)). The whole
diff --git a/docs/src/content/docs/producer/compile.md b/docs/src/content/docs/producer/compile.md
index 2ec5d3afb..eae0e1887 100644
--- a/docs/src/content/docs/producer/compile.md
+++ b/docs/src/content/docs/producer/compile.md
@@ -17,7 +17,7 @@ aggregated `AGENTS.md` / `copilot-instructions.md` it produces are a
 nice-to-have, not a requirement.
 
 Compile is **recommended for every other target** (`claude`,
-`cursor`, `codex`, `gemini`, `opencode`, `windsurf`) -- those
+`cursor`, `codex`, `gemini`, `opencode`, `windsurf`, `kiro`) -- those
 harnesses load instructions through the root context file
 (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`) or a harness-specific rules
 folder that compile generates. Without it, your instructions are
@@ -33,12 +33,12 @@ Concretely, that command rolls your `instructions/*.instructions.md`
 into the native rules surface each target expects:
 
 - `AGENTS.md` -- the cross-harness root context file (Copilot, Codex,
-  OpenCode, Windsurf all read this).
+  OpenCode, Windsurf, and Kiro all read this).
 - `CLAUDE.md` -- Claude Code's root context file.
 - `GEMINI.md` -- Gemini CLI's root context file.
 - per-harness rules trees that mirror each instruction's
   `applyTo:` glob: `.github/instructions/`, `.claude/rules/`,
-  `.cursor/rules/*.mdc`, `.windsurf/rules/`.
+  `.cursor/rules/*.mdc`, `.windsurf/rules/`, `.kiro/steering/`.
 
 Other primitive types -- prompts, skills, agents, chatmodes, hooks,
 commands -- are NOT compiled by this command. They are deployed by
@@ -87,7 +87,7 @@ apm compile --all                            # every canonical target
 ```
 
 Accepted values: `copilot`, `claude`, `cursor`, `opencode`, `codex`,
-`gemini`, `windsurf`, `agent-skills`, `all`. The `agent-skills` slug
+`gemini`, `windsurf`, `kiro`, `agent-skills`, `all`. The `agent-skills` slug
 is a no-op for compile (skills are deployed by `apm install`); it is
 accepted in target lists for symmetry only. Unknown slugs are
 rejected before any work runs.
@@ -100,7 +100,7 @@ order:
 1. Explicit `--target <slug>` flag.
 2. The `targets:` field in your `apm.yml`.
 3. Auto-detect: any harness root directory (`.github/`, `.claude/`,
-   `.cursor/`, `.codex/`, `.gemini/`, `.opencode/`, `.windsurf/`) that
+   `.cursor/`, `.codex/`, `.gemini/`, `.opencode/`, `.windsurf/`, `.kiro/`) that
    already exists.
 4. Fallback: `minimal` -- writes a single `AGENTS.md` and skips per-
    harness rules folders.
@@ -122,6 +122,7 @@ Per target, with the rules shape on disk after compile:
 | `gemini` | `GEMINI.md` (folded) | none -- compile-only, no per-file deploy | Yes -- folded into `GEMINI.md` |
 | `opencode` | `AGENTS.md` (folded) | none -- compile-only, no per-file deploy | Yes -- folded into `AGENTS.md` |
 | `windsurf` | -- | `.windsurf/rules/<name>.md` | Yes -- compiled to Windsurf rules |
+| `kiro` | `AGENTS.md` (folded) | `.kiro/steering/<name>.md` | Yes -- compiled to Kiro steering |
 
 ## compile vs install
 
diff --git a/docs/src/content/docs/reference/cli/compile.md b/docs/src/content/docs/reference/cli/compile.md
index 7cfc734f9..35ca8119d 100644
--- a/docs/src/content/docs/reference/cli/compile.md
+++ b/docs/src/content/docs/reference/cli/compile.md
@@ -35,7 +35,7 @@ for the full reach map.
 `.github/instructions/*.instructions.md` (with their `applyTo:`
 frontmatter) that `apm install` already deploys. Compile is
 **recommended for every other target** (`claude`, `cursor`, `codex`,
-`gemini`, `opencode`, `windsurf`), which load instructions through a
+`gemini`, `opencode`, `windsurf`, `kiro`), which load instructions through a
 root context file or harness-specific rules folder that compile
 generates.
 
@@ -44,7 +44,7 @@ Resolution order for which targets to compile:
 1. `--target` / `--all` on the command line
 2. `targets:` field in `apm.yml`
 3. Auto-detection from existing folders (`.github/`, `.claude/`,
-   `.codex/`, `.gemini/`, `.windsurf/`)
+   `.codex/`, `.gemini/`, `.windsurf/`, `.kiro/`)
 
 Use [`apm targets`](../targets/) to preview what auto-detection
 resolves to before compiling.
@@ -59,7 +59,7 @@ written. Critical findings cause the command to exit non-zero. See
 
 | Flag | Description |
 |------|-------------|
-| `-t, --target VALUE` | Target(s) to compile. Comma-separated. Values: `copilot`, `claude`, `cursor`, `opencode`, `codex`, `gemini`, `windsurf`, `agent-skills`, `all`. |
+| `-t, --target VALUE` | Target(s) to compile. Comma-separated. Values: `copilot`, `claude`, `cursor`, `opencode`, `codex`, `gemini`, `windsurf`, `kiro`, `agent-skills`, `all`. |
 | `--all` | Compile for all canonical targets. Equivalent to `--target all` and mutually exclusive with `--target`. Preferred form. |
 
 `vscode` and `agents` are accepted as deprecated aliases for `copilot`
@@ -199,6 +199,7 @@ one-shot `apm compile`; `--output` only applies in single-file mode.
 | `opencode` | `AGENTS.md` |
 | `gemini` | `AGENTS.md`, `GEMINI.md` |
 | `windsurf` | `AGENTS.md` |
+| `kiro` | `AGENTS.md` |
 | `agent-skills` | none |
 | `all` | `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `.github/copilot-instructions.md` |
 
diff --git a/docs/src/content/docs/reference/cli/deps.md b/docs/src/content/docs/reference/cli/deps.md
index 49daaeb2b..ee2119bca 100644
--- a/docs/src/content/docs/reference/cli/deps.md
+++ b/docs/src/content/docs/reference/cli/deps.md
@@ -107,7 +107,7 @@ apm deps update [PACKAGES...] [OPTIONS]
 |---|---|
 | `-v, --verbose` | Show detailed update information. |
 | `--force` | Overwrite locally-authored files on collision. |
-| `-t, --target` | Force deployment to specific targets. Comma-separated. Values: `copilot`, `claude`, `cursor`, `opencode`, `codex`, `gemini`, `windsurf`, `agent-skills`, `all`. `copilot-cowork` is also accepted when its experimental flag is enabled. `agent-skills` deploys to `.agents/skills/` (cross-client). `all` covers every per-client target but excludes `agent-skills`; combine to get both. |
+| `-t, --target` | Force deployment to specific targets. Comma-separated. Values: `copilot`, `claude`, `cursor`, `opencode`, `codex`, `gemini`, `windsurf`, `kiro`, `agent-skills`, `all`. `copilot-cowork` is also accepted when its experimental flag is enabled. `agent-skills` deploys to `.agents/skills/` (cross-client). `all` covers every per-client target but excludes `agent-skills`; combine to get both. |
 | `--parallel-downloads N` | Max concurrent downloads. Default `4`. `0` disables parallelism. |
 | `-g, --global` | Update user-scope dependencies in `~/.apm/`. |
 | `--legacy-skill-paths` | Deploy skill files to per-client paths (`.cursor/skills/`, etc.) instead of the shared `.agents/skills/` directory. |
diff --git a/docs/src/content/docs/reference/cli/init.md b/docs/src/content/docs/reference/cli/init.md
index ddea010c8..d4ca6786c 100644
--- a/docs/src/content/docs/reference/cli/init.md
+++ b/docs/src/content/docs/reference/cli/init.md
@@ -35,7 +35,7 @@ and [`apm marketplace init`](../marketplace/) instead.
 | `-y`, `--yes` | off | Skip interactive prompts; use auto-detected defaults. Overwrites an existing `apm.yml` without confirmation. |
 | `--plugin` | off | **Deprecated.** Use [`apm plugin init`](../plugin/) instead. Scaffold a plugin authoring project: also writes `plugin.json` and adds a `devDependencies` block to `apm.yml`. Plugin name must be kebab-case, max 64 chars. |
 | `--marketplace` | off | **Deprecated.** Use [`apm marketplace init`](../marketplace/) instead. Append a `marketplace:` authoring block to `apm.yml`. See [Publish to a marketplace](../../../producer/publish-to-a-marketplace/). |
-| `--target` | (prompt) | Comma-separated target list. Skips the interactive target prompt and writes targets directly. Valid values: `copilot`, `claude`, `cursor`, `opencode`, `codex`, `gemini`, `windsurf`. |
+| `--target` | (prompt) | Comma-separated target list. Skips the interactive target prompt and writes targets directly. Valid values: `copilot`, `claude`, `cursor`, `opencode`, `codex`, `gemini`, `windsurf`, `kiro`. |
 | `-v`, `--verbose` | off | Show detailed output. |
 
 Target precedence: `--target` flag > interactive prompt > auto-detect at
diff --git a/docs/src/content/docs/reference/cli/install.md b/docs/src/content/docs/reference/cli/install.md
index 4cbaf385c..a953e9a90 100644
--- a/docs/src/content/docs/reference/cli/install.md
+++ b/docs/src/content/docs/reference/cli/install.md
@@ -46,11 +46,11 @@ With no arguments it installs everything from `apm.yml`. With one or more `PACKA
 
 | Flag | Default | Description |
 |---|---|---|
-| `--target`, `-t VALUE` | auto-detect | Force deployment targets. Comma-separated for multiple (`-t claude,cursor`). Values: `copilot`, `claude`, `cursor`, `opencode`, `codex`, `gemini`, `windsurf`, `intellij`, `agent-skills`, `all`; experimental `copilot-cowork` and `copilot-app` are also accepted when enabled. `all` expands to every harness above except `agent-skills`; combine `all,agent-skills` for both. Highest precedence in the chain `--target` > `apm.yml targets:` > auto-detect. With nothing to detect, install exits `2` with a teaching message. |
+| `--target`, `-t VALUE` | auto-detect | Force deployment targets. Comma-separated for multiple (`-t claude,cursor`). Values: `copilot`, `claude`, `cursor`, `opencode`, `codex`, `gemini`, `windsurf`, `kiro`, `intellij`, `agent-skills`, `all`; experimental `copilot-cowork` and `copilot-app` are also accepted when enabled. `all` expands to every harness above except `agent-skills`; combine `all,agent-skills` for both. Highest precedence in the chain `--target` > `apm.yml targets:` > auto-detect. With nothing to detect, install exits `2` with a teaching message. |
 | `--runtime VALUE` | unset | Legacy alias for `--target` (single value only). Still accepted; prefer `--target`. |
 | `--exclude VALUE` | unset | Skip a single runtime that auto-detect or `targets:` would otherwise enable. |
 | `--only apm\|mcp` | both | Install only APM packages or only MCP servers. |
-| `-g`, `--global` | off | Install to user scope (`~/.apm/`) instead of the current project. MCP servers deploy only to global-capable runtimes (Copilot CLI, Codex CLI, JetBrains Copilot). |
+| `-g`, `--global` | off | Install to user scope (`~/.apm/`) instead of the current project. MCP servers deploy only to global-capable runtimes (Copilot CLI, Codex CLI, Gemini CLI, Kiro, JetBrains Copilot). |
 | `--legacy-skill-paths` | off | Deploy skills to per-client paths (`.cursor/skills/`, `.github/skills/`, ...) instead of the converged `.agents/skills/`. Env: `APM_LEGACY_SKILL_PATHS=1`. |
 
 ### Policy and trust
@@ -199,7 +199,7 @@ apm install owner/skill-bundle --skill '*'   # reset to all skills
 
 - **`--force` is dual-purpose.** It overwrites locally-authored files on collision **and** disables the critical-finding block from the built-in security scan. It does **not** suppress general install errors -- any error reported in the diagnostic summary still exits `1` (matches `npm` / `pip` / `cargo`). It does **not** refresh remote refs -- for routine ref updates, run [`apm update`](../update/). To remediate findings, prefer `apm audit --strip`. See [Drift and secure by default](../../../consumer/drift-and-secure-by-default/).
 - **Claude target prompt rewrite.** When deploying to `.claude/commands/`, prompt files with an `input:` front-matter key are rewritten to Claude's `arguments:` shape and `${input:name}` placeholders become `$name`. Argument names must match `^[A-Za-z][\w-]{0,63}$`; rejected names are dropped with a warning.
-- **MCP env-var passthrough.** Copilot CLI translates `${env:VAR}` and `<VAR>` to `${VAR}` in `~/.copilot/mcp-config.json`. JetBrains Copilot preserves env references as `${env:VAR}` in `github-copilot/intellij/mcp.json`. Plaintext secrets are never written to disk for these runtime-resolved targets; legacy targets resolve placeholders at install time.
+- **MCP env-var passthrough.** Copilot CLI and Kiro translate `${env:VAR}` and `<VAR>` to `${VAR}` in their MCP configs. JetBrains Copilot preserves env references as `${env:VAR}` in `github-copilot/intellij/mcp.json`. Plaintext secrets are never written to disk for these runtime-resolved targets; legacy targets resolve placeholders at install time.
 
 ### Install from a private registry (experimental)
 
diff --git a/docs/src/content/docs/reference/cli/targets.md b/docs/src/content/docs/reference/cli/targets.md
index 2bade9d03..ef53af7e4 100644
--- a/docs/src/content/docs/reference/cli/targets.md
+++ b/docs/src/content/docs/reference/cli/targets.md
@@ -75,6 +75,7 @@ Sample output in a project with `CLAUDE.md` and `.cursor/`:
   gemini       inactive   needs GEMINI.md                          .gemini/
   opencode     inactive   needs .opencode/                         .opencode/
   windsurf     inactive   needs .windsurf/                         .windsurf/
+  kiro         inactive   needs .kiro/                             .kiro/
 ```
 
 Machine-readable form:
@@ -98,6 +99,7 @@ match per target is enough to activate it.
 | `gemini` | `.gemini/` directory, or `GEMINI.md` file | `.gemini/` |
 | `opencode` | `.opencode/` directory | `.opencode/` |
 | `windsurf` | `.windsurf/` directory | `.windsurf/` |
+| `kiro` | `.kiro/` directory | `.kiro/` |
 | `agent-skills` | Meta-target; never auto-detected. Opt in via `targets:` in `apm.yml` or `--target agent-skills` on `apm install` / `apm deps update` (compile is a no-op for this target). | `.agents/` |
 
 Notes:
diff --git a/docs/src/content/docs/reference/cli/uninstall.md b/docs/src/content/docs/reference/cli/uninstall.md
index cef8219b9..88579440b 100644
--- a/docs/src/content/docs/reference/cli/uninstall.md
+++ b/docs/src/content/docs/reference/cli/uninstall.md
@@ -78,7 +78,7 @@ What gets removed, in order:
 1. The package entry in `apm.yml` under `dependencies.apm` or `devDependencies.apm`.
 2. The package folder under `apm_modules/owner/repo/`.
 3. Transitive dependencies that no remaining package depends on (npm-style pruning, computed from `apm.lock.yaml`).
-4. Every file in the lockfile's `deployed_files` for the removed packages and pruned orphans, across all harness folders (`.github/`, `.claude/`, `.cursor/`, `.opencode/`, `.gemini/`, `.codex/`, `.windsurf/`).
+4. Every file in the lockfile's `deployed_files` for the removed packages and pruned orphans, across all harness folders (`.github/`, `.claude/`, `.cursor/`, `.opencode/`, `.gemini/`, `.codex/`, `.windsurf/`, `.kiro/`).
 5. Hook entries inside `.claude/settings.json`, `.cursor/hooks.json`, and `.gemini/settings.json` that the removed packages contributed.
 6. MCP servers contributed only by the removed packages.
 7. The lockfile entries themselves. If no dependencies remain, `apm.lock.yaml` is deleted.
diff --git a/docs/src/content/docs/reference/cli/update.md b/docs/src/content/docs/reference/cli/update.md
index 9b2ee31f3..a511458ff 100644
--- a/docs/src/content/docs/reference/cli/update.md
+++ b/docs/src/content/docs/reference/cli/update.md
@@ -43,7 +43,7 @@ For a read-only install that pins to whatever is already in `apm.lock.yaml` -- t
 | `--global`, `-g` | off | Refresh user-scope dependencies under `~/.apm/` instead of the current project (mirrors `apm install -g`). |
 | `--force` | off | Overwrite locally-authored files on collision. |
 | `--parallel-downloads N` | `4` | Max concurrent package downloads. `0` disables parallelism. |
-| `--target TARGET`, `-t TARGET` | auto-detect | Agent harness(es) to update for. Accepts a single value (`claude`, `copilot`, `cursor`, `windsurf`, `codex`, `opencode`, `gemini`) or comma-separated list (`--target claude,cursor`). Overrides `apm.yml targets:` and auto-detection. |
+| `--target TARGET`, `-t TARGET` | auto-detect | Agent harness(es) to update for. Accepts a single value (`claude`, `copilot`, `cursor`, `windsurf`, `kiro`, `codex`, `opencode`, `gemini`) or comma-separated list (`--target claude,cursor`). Overrides `apm.yml targets:` and auto-detection. |
 
 ## Examples
 
diff --git a/docs/src/content/docs/reference/manifest-schema.md b/docs/src/content/docs/reference/manifest-schema.md
index e0545d655..8d5df6a0d 100644
--- a/docs/src/content/docs/reference/manifest-schema.md
+++ b/docs/src/content/docs/reference/manifest-schema.md
@@ -124,11 +124,11 @@ Newly initialised projects (`apm init`) are scaffolded by the CLI; see [`apm ini
 | **Type** | `string` or `list<string>` |
 | **Required** | OPTIONAL |
 | **Default** | Auto-detect from folder presence (see below). |
-| **Allowed values** | `vscode`, `agents`, `copilot`, `claude`, `cursor`, `opencode`, `codex`, `gemini`, `windsurf`, `all` |
+| **Allowed values** | `vscode`, `agents`, `copilot`, `claude`, `cursor`, `opencode`, `codex`, `gemini`, `windsurf`, `kiro`, `all` |
 
 Controls which output targets are generated during compilation, installation, and packing. Accepts a single string or a YAML list. Unknown values MUST raise a parse error at load time, naming the offending token.
 
-When `target:` is omitted, a conforming resolver SHOULD auto-detect: `vscode` if `.github/` exists, `claude` if `.claude/` exists, `codex` if `.codex/` exists, `windsurf` if `.windsurf/` exists, `all` if multiple are present, `minimal` if none. Auto-detection applies only when `target:` is unset; once set, the field is authoritative.
+When `target:` is omitted, a conforming resolver SHOULD auto-detect: `vscode` if `.github/` exists, `claude` if `.claude/` exists, `codex` if `.codex/` exists, `windsurf` if `.windsurf/` exists, `kiro` if `.kiro/` exists, `all` if multiple are present, `minimal` if none. Auto-detection applies only when `target:` is unset; once set, the field is authoritative.
 
 ```yaml
 # Single target
@@ -158,6 +158,7 @@ A plural alias `targets:` (YAML list only) is also accepted and takes precedence
 | `codex` | Emits `AGENTS.md` and deploys skills to `.agents/skills/`, agents to `.codex/agents/`. |
 | `gemini` | Emits `GEMINI.md` and deploys to `.gemini/commands/`, `.gemini/skills/`, `.gemini/settings.json`. |
 | `windsurf` | Emits `AGENTS.md` and deploys to `.windsurf/rules/`, `.windsurf/skills/`, `.windsurf/workflows/`, `.windsurf/hooks.json`. |
+| `kiro` | Emits `AGENTS.md` and deploys to `.kiro/steering/`, `.kiro/skills/`, `.kiro/hooks/`, `.kiro/settings/mcp.json`. |
 | `all` | All targets. Cannot be combined with other values in a list. |
 | `minimal` | `AGENTS.md` only at project root. **Auto-detected only**: this value MUST NOT be set explicitly in manifests; it is an internal fallback when no target folder is detected. |
 
@@ -531,7 +532,7 @@ dependencies:
 
 Values in `headers` and `env` may contain three placeholder syntaxes. APM resolves them per-target so secrets stay out of generated config files where possible.
 
-| Syntax | Source | VS Code | JetBrains Copilot | Copilot CLI | Codex / Gemini / Cursor |
+| Syntax | Source | VS Code | JetBrains Copilot | Copilot CLI / Kiro | Codex / Gemini / Cursor |
 |---|---|---|---|---|---|
 | `${VAR}` | host environment | Translated to `${env:VAR}` (resolved at server-start by VS Code) | Translated to `${env:VAR}` | Native; passed through verbatim | Resolved at install time from env (or interactive prompt) |
 | `${env:VAR}` | host environment | Native; passed through verbatim | Native; passed through verbatim | Translated to `${VAR}` | Resolved at install time from env (or interactive prompt) |
@@ -540,7 +541,7 @@ Values in `headers` and `env` may contain three placeholder syntaxes. APM resolv
 
 - **VS Code** has native `${env:VAR}` and `${input:VAR}` interpolation, so APM emits placeholders rather than baking secrets into `mcp.json`. Bare `${VAR}` is normalized to `${env:VAR}` for you.
 - **JetBrains Copilot** has native `${env:VAR}` interpolation in `mcp.json`; APM normalizes `${VAR}` and legacy `<VAR>` to `${env:VAR}`.
-- **Copilot CLI** has native `${VAR}` interpolation in `~/.copilot/mcp-config.json`; APM normalizes `${env:VAR}` and legacy `<VAR>` to `${VAR}`.
+- **Copilot CLI and Kiro** have native `${VAR}` interpolation in their MCP config files; APM normalizes `${env:VAR}` and legacy `<VAR>` to `${VAR}`.
 - **Codex, Gemini, and Cursor** have no runtime interpolation, so APM resolves `${VAR}`, `${env:VAR}`, and the legacy `<VAR>` at install time using `os.environ` (or an interactive prompt when missing). Resolved values are not re-scanned, so a value containing literal `${...}` text is preserved.
 - **Recommended:** Use `${VAR}` or `${env:VAR}` in all new manifests - they work on every target that supports remote MCP servers. `<VAR>` is legacy; in VS Code it would silently render as literal text in the generated config.
 - **Registry-backed servers** - APM auto-generates input prompts from registry metadata for `${input:...}`.
diff --git a/docs/src/content/docs/reference/targets-matrix.md b/docs/src/content/docs/reference/targets-matrix.md
index cc762e874..c73a12a89 100644
--- a/docs/src/content/docs/reference/targets-matrix.md
+++ b/docs/src/content/docs/reference/targets-matrix.md
@@ -26,11 +26,12 @@ see [Primitive types](./primitive-types/).
 | gemini          | `.gemini/`             |     [ ]      |   [ ]   |  [ ]   |  [x]   |   [x]    |  [x]  | [x] |
 | opencode        | `.opencode/`           |     [ ]      |   [ ]   |  [x]   |  [x]   |   [x]    |  [ ]  | [x] |
 | windsurf        | `.windsurf/`           |     [x]      |   [ ]   |  [ ]   |  [x]   |   [x]    |  [x]  | [x] |
+| kiro            | `.kiro/`               |     [x]      |   [ ]   |  [ ]   |  [x]   |   [ ]    |  [x]  | [x] |
 | agent-skills    | `.agents/`             |     [ ]      |   [ ]   |  [ ]   |  [x]   |   [ ]    |  [ ]  | [ ] |
 
 Skills deploy to `.agents/skills/` for Copilot, Cursor, OpenCode,
 Gemini, and Codex by default (see [Skills convergence](#skills-convergence)
-below). Claude and Windsurf keep target-native skill directories.
+below). Claude, Windsurf, and Kiro keep target-native skill directories.
 
 `copilot-cowork` (Microsoft 365 Copilot), `copilot-app` (GitHub
 Copilot desktop App), and `openclaw` (OpenClaw agent runtime) are
@@ -61,6 +62,7 @@ list before `compile` or `install`.
 | gemini   | `.gemini/` directory, or `GEMINI.md` file     |
 | opencode | `.opencode/` directory                        |
 | windsurf | `.windsurf/` directory                        |
+| kiro     | `.kiro/` directory                            |
 
 `agent-skills` is never auto-detected but is a canonical target: select it
 with `--target` or list it in a project's `apm.yml` `targets:` field so
@@ -172,6 +174,21 @@ Windsurf / Cascade.
 - **Agents.** Not deployed. Cascade auto-invokes any `SKILL.md` by its `description:` frontmatter, so a separate agents primitive would collide with skills on the same path. Ship personas as skills under `.apm/skills/<name>/SKILL.md` instead.
 - **User scope.** Partial. `instructions` is excluded at user scope; Windsurf stores global memory in a single `~/.codeium/windsurf/memories/global_rules.md` file with a different format.
 
+## kiro
+
+Kiro IDE.
+
+- **Detection.** `.kiro/` directory.
+- **Deploy directory.** `.kiro/` (project and user scope).
+- **Supported primitives.** instructions, skills, hooks, mcp.
+- **File conventions.**
+  - instructions: `.kiro/steering/<name>.md` with `inclusion: always` or `inclusion: fileMatch` frontmatter
+  - skills: `.kiro/skills/<name>/SKILL.md`
+  - hooks: one JSON file per hook under `.kiro/hooks/`
+  - mcp: `.kiro/settings/mcp.json` (project) or `~/.kiro/settings/mcp.json` (user)
+- **MCP shape.** JSON `mcpServers` entries use `command`/`args`/`env` for stdio and `url`/`headers` for remote servers. Kiro resolves `${VAR}` placeholders at runtime, so APM preserves them rather than writing secrets to disk.
+- **Scope.** This is the documented Kiro IDE layout only. Kiro CLI differences are tracked separately and are not part of this target.
+
 ## agent-skills
 
 Cross-client shared skills directory.
@@ -209,7 +226,7 @@ To restore the pre-convergence per-target layout (skills land under each target'
 
 MCP is not a `TargetProfile` primitive; it is wired by a separate
 integrator that writes per-client config files (e.g.
-`.vscode/mcp.json`, `.cursor/mcp.json`, `.claude.json`) for every
+`.vscode/mcp.json`, `.cursor/mcp.json`, `.claude.json`, `.kiro/settings/mcp.json`) for every
 target in the active set that has an MCP client adapter. Active set
 follows the same `--target` > `targets:` > auto-detect chain as
 `apm install`: a runtime with an adapter but outside the active set
diff --git a/docs/src/content/docs/troubleshooting/compile-zero-output-warning.md b/docs/src/content/docs/troubleshooting/compile-zero-output-warning.md
index 8f0bbc934..f285cb5d4 100644
--- a/docs/src/content/docs/troubleshooting/compile-zero-output-warning.md
+++ b/docs/src/content/docs/troubleshooting/compile-zero-output-warning.md
@@ -25,7 +25,7 @@ apm targets
 
 If the active list is empty, no harness was detected and no `target:` was pinned. APM has nothing to write against. Either:
 
-- Create one of the canonical signals (`.claude/`, `.github/copilot-instructions.md` or any of `.github/instructions/`, `.github/agents/`, `.github/prompts/`, `.github/hooks/`, `.cursor/`, `.codex/`, `.gemini/` or `GEMINI.md`, `.opencode/`, `.windsurf/`).
+- Create one of the canonical signals (`.claude/`, `.github/copilot-instructions.md` or any of `.github/instructions/`, `.github/agents/`, `.github/prompts/`, `.github/hooks/`, `.cursor/`, `.codex/`, `.gemini/` or `GEMINI.md`, `.opencode/`, `.windsurf/`, `.kiro/`).
 - Pin targets in `apm.yml` (`target: [claude, copilot]`) or pass `--target` to `apm compile`.
 
 See [`apm targets`](../reference/cli/targets/) and the [manifest schema](../reference/manifest-schema/).
diff --git a/packages/apm-guide/.apm/skills/apm-usage/commands.md b/packages/apm-guide/.apm/skills/apm-usage/commands.md
index c4d36f36a..2a9bd97c9 100644
--- a/packages/apm-guide/.apm/skills/apm-usage/commands.md
+++ b/packages/apm-guide/.apm/skills/apm-usage/commands.md
@@ -10,7 +10,7 @@
 
 | Command | Purpose | Key flags |
 |---------|---------|-----------|
-| `apm install [PKGS...]` | Install APM and MCP dependencies (supports APM packages, Claude skills (SKILL.md), and plugin collections (plugin.json)) | `--update` (deprecated; prefer `apm update`) refresh refs, `--refresh` re-fetch all deps from upstream and re-resolve all ref pins, `--force` overwrite (does NOT refresh refs; use `apm update` for that), `--frozen` CI-safe install that fails fast when `apm.lock.yaml` is missing or out of sync with `apm.yml` (mutually exclusive with `--update`; structural presence check only -- use `apm audit` for SHA integrity), `--dry-run`, `--verbose`, `--only [apm\|mcp]`, `--target` (comma-separated, e.g. `--target claude,cursor`; highest-priority entry in the resolution chain `--target` > apm.yml `targets:` > auto-detect; `--target all` deprecated, see `apm compile --all`; use `copilot-cowork` with `--global` after `apm experimental enable copilot-cowork`), `--dev`, `-g` global (MCP deploys only to user-scope runtimes: Copilot CLI, Codex CLI, JetBrains Copilot), `--trust-transitive-mcp`, `--parallel-downloads N`, `--allow-insecure`, `--allow-insecure-host HOSTNAME`, `--skill NAME` install named skill(s) from a skill collection (SKILL_BUNDLE or plugin manifest; repeatable; plugin manifests accept a leaf name or manifest path; persisted in apm.yml; `'*'` resets to all), `--legacy-skill-paths` restore per-client skill dirs, `--mcp NAME` add MCP entry (NAME goes through the same `--target` > `targets:` > auto-detect resolver as APM packages, so a project whitelisting `targets: [copilot]` will not write `.cursor/mcp.json` even if `.cursor/` exists; `apm install -g --mcp NAME` writes user-scope and bypasses the project-scope gate by design), `--transport`, `--url`, `--env KEY=VAL`, `--header KEY=VAL`, `--mcp-version`, `--registry URL` custom MCP registry, `--root DIR` redirect writes (`apm_modules/`, lockfile, `.gitignore`, integrated harness files) under DIR while `apm.yml`/`.apm/`/local deps resolve from `$PWD` (mirrors `pip install --target`; created if missing; not valid with `-g`/`--global`, which exits 2) |
+| `apm install [PKGS...]` | Install APM and MCP dependencies (supports APM packages, Claude skills (SKILL.md), and plugin collections (plugin.json)) | `--update` (deprecated; prefer `apm update`) refresh refs, `--refresh` re-fetch all deps from upstream and re-resolve all ref pins, `--force` overwrite (does NOT refresh refs; use `apm update` for that), `--frozen` CI-safe install that fails fast when `apm.lock.yaml` is missing or out of sync with `apm.yml` (mutually exclusive with `--update`; structural presence check only -- use `apm audit` for SHA integrity), `--dry-run`, `--verbose`, `--only [apm\|mcp]`, `--target` (comma-separated, e.g. `--target claude,cursor`; highest-priority entry in the resolution chain `--target` > apm.yml `targets:` > auto-detect; `--target all` deprecated, see `apm compile --all`; use `kiro` for Kiro IDE; use `copilot-cowork` with `--global` after `apm experimental enable copilot-cowork`), `--dev`, `-g` global (MCP deploys only to user-scope runtimes: Copilot CLI, Codex CLI, Gemini CLI, Kiro, JetBrains Copilot), `--trust-transitive-mcp`, `--parallel-downloads N`, `--allow-insecure`, `--allow-insecure-host HOSTNAME`, `--skill NAME` install named skill(s) from a skill collection (SKILL_BUNDLE or plugin manifest; repeatable; plugin manifests accept a leaf name or manifest path; persisted in apm.yml; `'*'` resets to all), `--legacy-skill-paths` restore per-client skill dirs, `--mcp NAME` add MCP entry (NAME goes through the same `--target` > `targets:` > auto-detect resolver as APM packages, so a project whitelisting `targets: [copilot]` will not write `.cursor/mcp.json` even if `.cursor/` exists; `apm install -g --mcp NAME` writes user-scope and bypasses the project-scope gate by design), `--transport`, `--url`, `--env KEY=VAL`, `--header KEY=VAL`, `--mcp-version`, `--registry URL` custom MCP registry, `--root DIR` redirect writes (`apm_modules/`, lockfile, `.gitignore`, integrated harness files) under DIR while `apm.yml`/`.apm/`/local deps resolve from `$PWD` (mirrors `pip install --target`; created if missing; not valid with `-g`/`--global`, which exits 2) |
 | `apm targets` | Show resolved deployment targets for the current project (Click group; reads filesystem signals; works with or without `apm.yml`) | `--all` also include the `agent-skills` meta-target (only meaningful with `--json`), `--json` machine-readable output. No provenance line is printed (the table is the provenance). |
 | `apm uninstall PKGS...` | Remove packages (accepts `owner/repo` or `name@marketplace`) | `--dry-run`, `-g` global |
 | `apm prune` | Remove orphaned packages | `--dry-run` |
@@ -34,7 +34,7 @@
 
 1. `--target` flag (highest; CSV form: `--target claude,cursor`).
 2. `apm.yml` `targets:` list (or singular `target:` sugar).
-3. Auto-detect from filesystem signals (`.claude/` or `CLAUDE.md` -> claude, `.cursor/` -> cursor, `.github/copilot-instructions.md` or any of `.github/instructions/`, `.github/agents/`, `.github/prompts/`, `.github/hooks/` -> copilot, `.codex/` -> codex, `.gemini/` or `GEMINI.md` -> gemini, `.opencode/` -> opencode, `.windsurf/` -> windsurf, the user-scope JetBrains Copilot MCP config directory `github-copilot/intellij/` -- `%LOCALAPPDATA%\github-copilot\intellij\` on Windows, `~/Library/Application Support/github-copilot/intellij/` on macOS, `~/.local/share/github-copilot/intellij/` on Linux -> intellij). All signals except JetBrains are project-scoped repo markers; the JetBrains signal is a machine-global user-scope directory, so once the Copilot plugin is installed it is detected for every project on that machine.
+3. Auto-detect from filesystem signals (`.claude/` or `CLAUDE.md` -> claude, `.cursor/` -> cursor, `.github/copilot-instructions.md` or any of `.github/instructions/`, `.github/agents/`, `.github/prompts/`, `.github/hooks/` -> copilot, `.codex/` -> codex, `.gemini/` or `GEMINI.md` -> gemini, `.opencode/` -> opencode, `.windsurf/` -> windsurf, `.kiro/` -> kiro, the user-scope JetBrains Copilot MCP config directory `github-copilot/intellij/` -- `%LOCALAPPDATA%\github-copilot\intellij\` on Windows, `~/Library/Application Support/github-copilot/intellij/` on macOS, `~/.local/share/github-copilot/intellij/` on Linux -> intellij). All signals except JetBrains are project-scoped repo markers; the JetBrains signal is a machine-global user-scope directory, so once the Copilot plugin is installed it is detected for every project on that machine.
 
 `apm install` prints a one-line provenance summary before any mutation:
 
diff --git a/packages/apm-guide/.apm/skills/apm-usage/dependencies.md b/packages/apm-guide/.apm/skills/apm-usage/dependencies.md
index de4902608..cbf66f22e 100644
--- a/packages/apm-guide/.apm/skills/apm-usage/dependencies.md
+++ b/packages/apm-guide/.apm/skills/apm-usage/dependencies.md
@@ -314,6 +314,7 @@ dependencies:
         #                            from host env at server-start (no plaintext on disk).
         #                            VS Code and JetBrains: rewritten to ${env:VAR}
         #                            and resolved at runtime.
+        #                            Kiro: preserved as ${VAR} and resolved at runtime.
         #                            Cursor/Windsurf/OpenCode/Claude/Gemini: resolved at install time.
         #                            Codex: passed through unchanged.
         #   ${input:<id>}         -> VS Code prompts user at runtime
diff --git a/packages/apm-guide/.apm/skills/apm-usage/package-authoring.md b/packages/apm-guide/.apm/skills/apm-usage/package-authoring.md
index 90ae009e1..bee4f70aa 100644
--- a/packages/apm-guide/.apm/skills/apm-usage/package-authoring.md
+++ b/packages/apm-guide/.apm/skills/apm-usage/package-authoring.md
@@ -98,6 +98,7 @@ hooks:
 | `*-codex-hooks.json` | Codex CLI only |
 | `*-gemini-hooks.json` | Gemini CLI only |
 | `*-windsurf-hooks.json` | Windsurf only |
+| `*-kiro-hooks.json` | Kiro only |
 | Any other name (e.g. `hooks.json`, `telemetry-hooks.json`) | All targets |
 
 Example directory tree for a multi-target hook package:
@@ -109,6 +110,7 @@ my-hooks-pkg/
     copilot-hooks.json      # Copilot only
     cursor-hooks.json       # Cursor only
     claude-hooks.json       # Claude Code only
+    kiro-hooks.json         # Kiro only
 ```
 
 APM automatically normalises event names per target (e.g. `postToolUse`
@@ -121,7 +123,7 @@ the correct target-specific form.
 `apm install` (project-scope, no `-g`) keeps hook `command` paths
 **repo-relative** in checked-in configs (`<repo>/.claude/settings.json`,
 `<repo>/.codex/hooks.json`, the `<repo>/.claude/apm-hooks.json`
-sidecar, and equivalents for Cursor / Gemini / Windsurf) so clones,
+sidecar, and equivalents for Cursor / Gemini / Windsurf / Kiro) so clones,
 contributors, and CI runners do not see the installer's machine-local
 absolute prefix. `apm install -g` (user-scope, e.g.
 `~/.claude/settings.json`) rewrites `${PLUGIN_ROOT}` and relative `./`
@@ -149,11 +151,11 @@ Both `apm.yml`'s `targets:`/`target:` and the `--target` CLI flag share the same
 | Form | Behaviour |
 |------|-----------|
 | `targets: [claude, copilot]` | Canonical list form; only listed targets are compiled/installed |
-| `target: copilot` | Singular sugar; allowed values: `vscode`, `agents`, `copilot`, `claude`, `cursor`, `opencode`, `codex`, `gemini`, `windsurf`, `all` |
+| `target: copilot` | Singular sugar; allowed values: `vscode`, `agents`, `copilot`, `claude`, `cursor`, `opencode`, `codex`, `gemini`, `windsurf`, `kiro`, `all` |
 | `target: claude,copilot` | CSV-string sugar; parses identically to the list form (the shared validator splits on `,`) |
 | `targets:` and `target:` both set | **Parse error** -- pick one |
 | `targets: []` (empty list) | **Parse error** -- remove the line if you meant auto-detect |
-| `targets:`/`target:` omitted | Resolution falls through to auto-detect from filesystem signals (`.claude/`, `CLAUDE.md`, `.cursor/`, `.cursorrules`, `.github/copilot-instructions.md`, `.github/instructions/`, `.github/agents/`, `.github/prompts/`, `.github/hooks/`, `.codex/`, `.gemini/`, `GEMINI.md`, `.opencode/`, `.windsurf/`) |
+| `targets:`/`target:` omitted | Resolution falls through to auto-detect from filesystem signals (`.claude/`, `CLAUDE.md`, `.cursor/`, `.cursorrules`, `.github/copilot-instructions.md`, `.github/instructions/`, `.github/agents/`, `.github/prompts/`, `.github/hooks/`, `.codex/`, `.gemini/`, `GEMINI.md`, `.opencode/`, `.windsurf/`, `.kiro/`) |
 | `target: bogus` (unknown token) | **Parse error** -- fix the typo |
 | `target: [all, claude]` (`all` mixed with other targets) | **Parse error** -- use `all` alone |
 
@@ -180,8 +182,8 @@ tags: [security, validation]
 accepted; when multiple sequence elements are given, the first is used.
 Commas inside brace alternation (`**/*.{css,scss}`) are part of the glob
 and are NOT separators -- only top-level commas split the list. On Copilot
-the value is preserved verbatim; on Claude/Cursor/Windsurf comma-lists are
-expanded to a YAML array under `paths:` / `globs:`.
+the value is preserved verbatim; on Claude/Cursor/Windsurf/Kiro comma-lists are
+expanded to a YAML array under `paths:` / `globs:` / `fileMatchPattern:`.
 
 ### 2. Chatmode (`*.chatmode.md`)
 
diff --git a/src/apm_cli/adapters/client/kiro.py b/src/apm_cli/adapters/client/kiro.py
new file mode 100644
index 000000000..739177def
--- /dev/null
+++ b/src/apm_cli/adapters/client/kiro.py
@@ -0,0 +1,220 @@
+"""Kiro IDE implementation of MCP client adapter.
+
+Kiro reads MCP configuration from ``.kiro/settings/mcp.json`` at project
+scope and ``~/.kiro/settings/mcp.json`` at user scope. The schema uses a
+``mcpServers`` object whose server entries support stdio ``command`` /
+``args`` / ``env`` and remote ``url`` / ``headers`` entries. Kiro resolves
+``${VAR}`` environment placeholders at runtime, so this adapter preserves
+placeholders instead of writing host secrets to disk.
+"""
+
+from __future__ import annotations
+
+import json
+import logging
+import os
+from pathlib import Path
+from typing import Any
+
+from ...utils.console import _rich_error, _rich_success
+from .copilot import CopilotClientAdapter
+
+logger = logging.getLogger(__name__)
+
+
+class KiroClientAdapter(CopilotClientAdapter):
+    """Kiro IDE MCP client adapter."""
+
+    supports_user_scope: bool = True
+    _client_label: str = "Kiro"
+    target_name: str = "kiro"
+    mcp_servers_key: str = "mcpServers"
+    _supports_runtime_env_substitution: bool = True
+
+    def _get_kiro_root(self) -> Path:
+        """Return the ``.kiro`` directory for the active scope."""
+        if self.user_scope:
+            return Path.home() / ".kiro"
+        return self.project_root / ".kiro"
+
+    def get_config_path(self) -> str:
+        """Return the Kiro MCP config path for the active scope."""
+        return str(self._get_kiro_root() / "settings" / "mcp.json")
+
+    def get_current_config(self) -> dict[str, Any]:
+        """Read the current Kiro MCP config."""
+        config_path = Path(self.get_config_path())
+        if not config_path.exists():
+            return {}
+        try:
+            with open(config_path, encoding="utf-8") as f:
+                data = json.load(f)
+            return data if isinstance(data, dict) else {}
+        except (OSError, json.JSONDecodeError) as exc:
+            logger.warning("Could not read %s: %s", config_path, exc)
+            return {}
+
+    def update_config(self, config_updates: dict[str, dict[str, Any]]) -> bool | None:
+        """Merge *config_updates* into Kiro's ``mcpServers`` object.
+
+        Project scope is opt-in: the workspace must already contain ``.kiro/``.
+        User scope creates ``~/.kiro/settings/`` on demand.
+        """
+        kiro_root = self._get_kiro_root()
+        if not self.user_scope and not kiro_root.is_dir():
+            logger.debug("Skipping Kiro project-scope write -- %s does not exist", kiro_root)
+            return None
+
+        config_path = Path(self.get_config_path())
+        current_config = self.get_current_config()
+        if not isinstance(current_config.get(self.mcp_servers_key), dict):
+            current_config[self.mcp_servers_key] = {}
+        current_config[self.mcp_servers_key].update(config_updates)
+
+        config_path.parent.mkdir(parents=True, exist_ok=True)
+        with open(config_path, "w", encoding="utf-8") as f:
+            json.dump(current_config, f, indent=2)
+        os.chmod(config_path, 0o600)
+        return True
+
+    @staticmethod
+    def _header_mapping(remote: dict[str, Any]) -> dict[str, Any]:
+        """Return registry remote headers as a dict."""
+        headers = remote.get("headers", {})
+        if isinstance(headers, list):
+            return {
+                h["name"]: h["value"]
+                for h in headers
+                if isinstance(h, dict) and "name" in h and "value" in h
+            }
+        if isinstance(headers, dict):
+            return dict(headers)
+        return {}
+
+    @staticmethod
+    def _copy_kiro_extensions(config: dict[str, Any], server_info: dict[str, Any]) -> None:
+        """Carry Kiro-specific MCP fields when the registry supplies them."""
+        for key in ("autoApprove", "disabledTools", "disabled"):
+            if key in server_info and server_info[key] is not None:
+                config[key] = server_info[key]
+
+    def _format_server_config(
+        self,
+        server_info: dict[str, Any],
+        env_overrides: dict[str, str] | None = None,
+        runtime_vars: dict[str, str] | None = None,
+    ) -> dict[str, Any]:
+        """Format registry or self-defined server info for Kiro."""
+        if runtime_vars is None:
+            runtime_vars = {}
+
+        raw = server_info.get("_raw_stdio")
+        if raw:
+            config: dict[str, Any] = {"command": raw["command"]}
+            resolved_env_for_args: dict[str, Any] = {}
+            if raw.get("env"):
+                resolved_env_for_args = self._resolve_environment_variables(
+                    raw["env"], env_overrides=env_overrides
+                )
+                config["env"] = resolved_env_for_args
+                self._warn_input_variables(raw["env"], server_info.get("name", ""), "Kiro")
+            config["args"] = [
+                self._resolve_variable_placeholders(arg, resolved_env_for_args, runtime_vars)
+                if isinstance(arg, str)
+                else arg
+                for arg in raw.get("args") or []
+            ]
+            self._copy_kiro_extensions(config, server_info)
+            return config
+
+        remotes = server_info.get("remotes", [])
+        if remotes:
+            remote = self._select_remote_with_url(remotes) or remotes[0]
+            transport = (remote.get("transport_type") or "").strip()
+            if not transport:
+                transport = "http"
+            elif transport not in ("sse", "http", "streamable-http"):
+                raise ValueError(
+                    f"Unsupported remote transport '{transport}' for Kiro. "
+                    f"Server: {server_info.get('name', 'unknown')}. "
+                    "Supported transports: http, sse, streamable-http."
+                )
+
+            config = {"url": (remote.get("url") or "").strip()}
+            headers = {
+                name: self._resolve_env_variable(name, value, env_overrides)
+                if isinstance(value, str)
+                else value
+                for name, value in self._header_mapping(remote).items()
+                if name
+            }
+            if headers:
+                config["headers"] = headers
+                self._warn_input_variables(headers, server_info.get("name", ""), "Kiro")
+            self._copy_kiro_extensions(config, server_info)
+            return config
+
+        packages = server_info.get("packages", [])
+        if not packages:
+            raise ValueError(
+                "MCP server has incomplete configuration in registry - "
+                "no package information or remote endpoints available. "
+                f"Server: {server_info.get('name', 'unknown')}"
+            )
+
+        config = {}
+        package = self._select_and_dispatch_best_package(
+            config,
+            packages,
+            env_overrides,
+            runtime_vars,
+        )
+        if not package:
+            raise ValueError(
+                f"No supported package type found for Kiro. "
+                f"Server: {server_info.get('name', 'unknown')}."
+            )
+        self._copy_kiro_extensions(config, server_info)
+        return config
+
+    def configure_mcp_server(
+        self,
+        server_url,
+        server_name=None,
+        enabled=True,
+        env_overrides=None,
+        server_info_cache=None,
+        runtime_vars=None,
+    ) -> bool:
+        """Configure an MCP server in Kiro's MCP config."""
+        if not server_url:
+            _rich_error("server_url cannot be empty", symbol="error")
+            return False
+
+        if not self.user_scope and not self._get_kiro_root().is_dir():
+            logger.debug(
+                "Kiro opt-in gate: %s absent, skipping configure_mcp_server",
+                self._get_kiro_root(),
+            )
+            return True
+
+        try:
+            server_info = self._fetch_server_info(server_url, server_info_cache)
+            if server_info is None:
+                return False
+
+            self._last_env_placeholder_keys = set()
+            self._last_legacy_angle_vars = set()
+
+            config_key = self._determine_config_key(server_url, server_name)
+            server_config = self._format_server_config(server_info, env_overrides, runtime_vars)
+            if not enabled:
+                server_config["disabled"] = True
+            self.update_config({config_key: server_config})
+
+            _rich_success(f"Configured MCP server '{config_key}' for Kiro", symbol="success")
+            return True
+        except Exception as exc:
+            logger.debug("Kiro MCP configuration failed: %s", exc)
+            _rich_error("Failed to configure MCP server for Kiro", symbol="error")
+            return False
diff --git a/src/apm_cli/compilation/agents_compiler.py b/src/apm_cli/compilation/agents_compiler.py
index 497e9adc7..0bedc28ca 100644
--- a/src/apm_cli/compilation/agents_compiler.py
+++ b/src/apm_cli/compilation/agents_compiler.py
@@ -48,6 +48,7 @@
     "agent-skills",
     "gemini",
     "windsurf",
+    "kiro",
     "all",
     "minimal",
 ) + _VSCODE_TARGET_ALIASES
diff --git a/src/apm_cli/core/apm_yml.py b/src/apm_cli/core/apm_yml.py
index 5a3aca33d..62dd89547 100644
--- a/src/apm_cli/core/apm_yml.py
+++ b/src/apm_cli/core/apm_yml.py
@@ -31,6 +31,7 @@
         "codex",
         "gemini",
         "windsurf",
+        "kiro",
         "agent-skills",
     }
 )
diff --git a/src/apm_cli/core/target_detection.py b/src/apm_cli/core/target_detection.py
index 2e106317e..7520156b3 100644
--- a/src/apm_cli/core/target_detection.py
+++ b/src/apm_cli/core/target_detection.py
@@ -1,7 +1,7 @@
 """Target detection for auto-selecting compilation and integration targets.
 
 This module implements the auto-detection pattern for determining which agent
-targets (Copilot, Claude, Cursor, OpenCode, Codex, Gemini) should be used
+targets (Copilot, Claude, Cursor, OpenCode, Codex, Gemini, Kiro) should be used
 based on existing project structure and configuration.
 
 Detection priority (highest to lowest):
@@ -58,6 +58,7 @@ def agents_alias_was_detected() -> bool:
     "codex",
     "gemini",
     "windsurf",
+    "kiro",
     "agent-skills",
     "all",
     "minimal",
@@ -97,6 +98,7 @@ def agents_alias_was_detected() -> bool:
     "codex",
     "gemini",
     "windsurf",
+    "kiro",
     "agent-skills",
     "all",
     "minimal",
@@ -136,6 +138,8 @@ def detect_target(  # noqa: PLR0911
             return "gemini", "explicit --target flag"
         elif explicit_target == "windsurf":
             return "windsurf", "explicit --target flag"
+        elif explicit_target == "kiro":
+            return "kiro", "explicit --target flag"
         elif explicit_target == "agent-skills":
             return "agent-skills", "explicit --target flag"
         elif explicit_target == "all":
@@ -157,6 +161,8 @@ def detect_target(  # noqa: PLR0911
             return "gemini", "apm.yml target"
         elif config_target == "windsurf":
             return "windsurf", "apm.yml target"
+        elif config_target == "kiro":
+            return "kiro", "apm.yml target"
         elif config_target == "agent-skills":
             return "agent-skills", "apm.yml target"
         elif config_target == "all":
@@ -170,6 +176,7 @@ def detect_target(  # noqa: PLR0911
     codex_exists = (project_root / ".codex").is_dir()
     gemini_exists = (project_root / ".gemini").is_dir()
     windsurf_exists = (project_root / ".windsurf").is_dir()
+    kiro_exists = (project_root / ".kiro").is_dir()
     detected = []
     if github_exists:
         detected.append(".github/")
@@ -185,6 +192,8 @@ def detect_target(  # noqa: PLR0911
         detected.append(".gemini/")
     if windsurf_exists:
         detected.append(".windsurf/")
+    if kiro_exists:
+        detected.append(".kiro/")
 
     if len(detected) >= 2:
         return "all", f"detected {' and '.join(detected)} folders"
@@ -202,6 +211,8 @@ def detect_target(  # noqa: PLR0911
         return "gemini", "detected .gemini/ folder"
     elif windsurf_exists:
         return "windsurf", "detected .windsurf/ folder"
+    elif kiro_exists:
+        return "kiro", "detected .kiro/ folder"
     else:
         return "minimal", REASON_NO_TARGET_FOLDER
 
@@ -221,7 +232,7 @@ def should_compile_agents_md(target: CompileTargetType) -> bool:
     """
     if isinstance(target, frozenset):
         return "agents" in target or "gemini" in target
-    return target in ("vscode", "opencode", "codex", "gemini", "windsurf", "all", "minimal")
+    return target in ("vscode", "opencode", "codex", "gemini", "windsurf", "kiro", "all", "minimal")
 
 
 def should_compile_claude_md(target: CompileTargetType) -> bool:
@@ -328,9 +339,10 @@ def get_target_description(target: UserTargetType) -> str:
         "codex": "AGENTS.md + .agents/skills/ + .codex/agents/ + .codex/hooks.json",
         "gemini": "GEMINI.md + .gemini/commands/ + .gemini/skills/ + .gemini/settings.json (MCP/hooks)",
         "windsurf": "AGENTS.md + .windsurf/rules/ + .windsurf/skills/ + .windsurf/workflows/ + .windsurf/hooks.json",
+        "kiro": "AGENTS.md + .kiro/steering/ + .kiro/skills/ + .kiro/hooks/",
         "agent-skills": ".agents/skills/ only (cross-client shared skills -- no agents, hooks, or commands)",
         "openclaw": ".agents/skills/ (project) or ~/.openclaw/skills/ (--global) -- experimental",
-        "all": "AGENTS.md + CLAUDE.md + GEMINI.md + .github/copilot-instructions.md + .github/ + .claude/ + .cursor/ + .opencode/ + .codex/ + .gemini/ + .windsurf/ + .agents/",
+        "all": "AGENTS.md + CLAUDE.md + GEMINI.md + .github/copilot-instructions.md + .github/ + .claude/ + .cursor/ + .opencode/ + .codex/ + .gemini/ + .windsurf/ + .kiro/ + .agents/",
         "minimal": "AGENTS.md only (create .github/, .claude/, or .gemini/ for full integration)",
     }
     return descriptions.get(normalized, "unknown target")
@@ -343,7 +355,7 @@ def get_target_description(target: UserTargetType) -> str:
 #: The complete set of real (non-pseudo) canonical targets.
 #: "minimal" is intentionally excluded -- it is a fallback pseudo-target.
 ALL_CANONICAL_TARGETS = frozenset(
-    {"vscode", "claude", "cursor", "opencode", "codex", "gemini", "windsurf"}
+    {"vscode", "claude", "cursor", "opencode", "codex", "gemini", "windsurf", "kiro"}
 )
 
 #: Targets that the parser must accept but that are gated at runtime by
@@ -662,6 +674,7 @@ class ResolvedTargets:
     ("gemini", "file", "GEMINI.md"),
     ("opencode", "dir", ".opencode"),
     ("windsurf", "dir", ".windsurf"),
+    ("kiro", "dir", ".kiro"),
 ]
 
 # Ordered list of targets for display (excludes agent-skills meta-target).
@@ -673,6 +686,7 @@ class ResolvedTargets:
     "gemini",
     "opencode",
     "windsurf",
+    "kiro",
 ]
 
 # Canonical deploy directories for each target.
@@ -684,6 +698,7 @@ class ResolvedTargets:
     "gemini": ".gemini/",
     "opencode": ".opencode/",
     "windsurf": ".windsurf/",
+    "kiro": ".kiro/",
 }
 
 # The primary (lowest-friction) signal for each target, used in
@@ -696,6 +711,7 @@ class ResolvedTargets:
     "gemini": "GEMINI.md",
     "opencode": ".opencode/",
     "windsurf": ".windsurf/",
+    "kiro": ".kiro/",
 }
 
 
diff --git a/src/apm_cli/factory.py b/src/apm_cli/factory.py
index 5578acacd..966756268 100644
--- a/src/apm_cli/factory.py
+++ b/src/apm_cli/factory.py
@@ -8,6 +8,7 @@
 from .adapters.client.cursor import CursorClientAdapter
 from .adapters.client.gemini import GeminiClientAdapter
 from .adapters.client.intellij import IntelliJClientAdapter
+from .adapters.client.kiro import KiroClientAdapter
 from .adapters.client.opencode import OpenCodeClientAdapter
 from .adapters.client.vscode import VSCodeClientAdapter
 from .adapters.client.windsurf import WindsurfClientAdapter
@@ -26,6 +27,7 @@
     "cursor": CursorClientAdapter,
     "gemini": GeminiClientAdapter,
     "intellij": IntelliJClientAdapter,
+    "kiro": KiroClientAdapter,
     "opencode": OpenCodeClientAdapter,
     "windsurf": WindsurfClientAdapter,
     "claude": ClaudeClientAdapter,
diff --git a/src/apm_cli/integration/hook_integrator.py b/src/apm_cli/integration/hook_integrator.py
index 122fe4f05..eaac2744b 100644
--- a/src/apm_cli/integration/hook_integrator.py
+++ b/src/apm_cli/integration/hook_integrator.py
@@ -112,6 +112,24 @@ class _MergeHookConfig:
         "postToolUse": "AfterTool",
         "Stop": "SessionEnd",
     },
+    "kiro": {
+        # Copilot / Claude -> Kiro camelCase events
+        "PreToolUse": "preToolUse",
+        "preToolUse": "preToolUse",
+        "PostToolUse": "postToolUse",
+        "postToolUse": "postToolUse",
+        "UserPromptSubmit": "promptSubmit",
+        "userPromptSubmit": "promptSubmit",
+        "promptSubmit": "promptSubmit",
+        "Stop": "agentStop",
+        "stop": "agentStop",
+        "AgentStop": "agentStop",
+        "agentStop": "agentStop",
+        "PreTaskExecution": "preTaskExecution",
+        "preTaskExecution": "preTaskExecution",
+        "PostTaskExecution": "postTaskExecution",
+        "postTaskExecution": "postTaskExecution",
+    },
 }
 
 # Expected hook event naming convention per target.
@@ -125,6 +143,7 @@ class _MergeHookConfig:
     "codex": "PascalCase",
     "gemini": "PascalCase",
     "windsurf": "PascalCase",
+    "kiro": "camelCase",
 }
 
 
@@ -318,6 +337,7 @@ def _reinject_apm_source_from_sidecar(hooks: dict, sidecar_data: dict) -> None:
     "codex-hooks": {"codex"},
     "gemini-hooks": {"gemini"},
     "windsurf-hooks": {"windsurf"},
+    "kiro-hooks": {"kiro"},
 }
 
 
@@ -607,6 +627,9 @@ def _rewrite_command_for_target(
         elif target == "windsurf":
             base_root = root_dir or ".windsurf"
             scripts_base = f"{base_root}/hooks/{package_name}"
+        elif target == "kiro":
+            base_root = root_dir or ".kiro"
+            scripts_base = f"{base_root}/hooks/{package_name}"
         else:
             base_root = root_dir or ".claude"
             scripts_base = f"{base_root}/hooks/{package_name}"
@@ -615,7 +638,8 @@ def _rewrite_command_for_target(
         # Match both forward-slash and backslash separators (Windows hook JSON
         # may use backslashes: ${CLAUDE_PLUGIN_ROOT}\scripts\scan.ps1)
         plugin_root_pattern = (
-            r"\$\{(?:CLAUDE_PLUGIN_ROOT|CURSOR_PLUGIN_ROOT|PLUGIN_ROOT)\}([\\/][^\s\"']+)"
+            r"\$\{(?:CLAUDE_PLUGIN_ROOT|CURSOR_PLUGIN_ROOT|KIRO_PLUGIN_ROOT|PLUGIN_ROOT)\}"
+            r"([\\/][^\s\"']+)"
         )
         for match in re.finditer(plugin_root_pattern, command):
             full_var = match.group(0)
@@ -1664,6 +1688,20 @@ def integrate_hooks_for_target(
                 target=target,
             )
 
+        if target.name == "kiro":
+            from apm_cli.integration.kiro_hook_integrator import integrate_kiro_hooks
+
+            return integrate_kiro_hooks(
+                self,
+                package_info,
+                project_root,
+                force=force,
+                managed_files=managed_files,
+                diagnostics=diagnostics,
+                target=target,
+                user_scope=user_scope,
+            )
+
         config = _MERGE_HOOK_TARGETS.get(target.name)
         if config is not None:
             return self._integrate_merged_hooks(
diff --git a/src/apm_cli/integration/instruction_integrator.py b/src/apm_cli/integration/instruction_integrator.py
index 7b80e648f..35da488e9 100644
--- a/src/apm_cli/integration/instruction_integrator.py
+++ b/src/apm_cli/integration/instruction_integrator.py
@@ -41,6 +41,7 @@ class InstructionIntegrator(BaseIntegrator):
         "cursor_rules": "_convert_to_cursor_rules",
         "claude_rules": "_convert_to_claude_rules",
         "windsurf_rules": "_convert_to_windsurf_rules",
+        "kiro_steering": "_convert_to_kiro_steering",
     }
 
     def find_instruction_files(self, package_path: Path) -> list[Path]:
@@ -263,6 +264,9 @@ def sync_for_target(
                 # Do not use a broad legacy glob for Claude rules to avoid
                 # deleting user-authored .md files under .claude/rules/.
                 legacy_pattern = None
+            elif mapping.format_id == "kiro_steering":
+                # Do not delete user-authored steering markdown under .kiro/steering/.
+                legacy_pattern = None
             else:
                 legacy_pattern = "*.instructions.md"
         return self.sync_remove_files(
@@ -609,6 +613,54 @@ def copy_instruction_windsurf(self, source: Path, target: Path) -> int:
         target.write_text(content, encoding="utf-8")
         return links_resolved
 
+    # ------------------------------------------------------------------
+    # Kiro Steering (.md with inclusion frontmatter)
+    # ------------------------------------------------------------------
+
+    @staticmethod
+    def _convert_to_kiro_steering(content: str) -> str:
+        """Convert APM instructions to Kiro steering format.
+
+        Kiro steering files use ``inclusion: always`` for unconditional
+        guidance and ``inclusion: fileMatch`` plus ``fileMatchPattern`` for
+        path-scoped guidance. APM's ``applyTo`` frontmatter is the source of
+        truth for that scoping.
+        """
+        import yaml
+
+        body = content
+        apply_to = ""
+
+        fm_match = re.match(r"^---\s*\r?\n(.*?)\r?\n---\s*\r?\n?", content, re.DOTALL)
+        if fm_match:
+            body = content[fm_match.end() :]
+            try:
+                fm = yaml.safe_load(fm_match.group(1)) or {}
+            except Exception:
+                fm = {}
+            raw_apply_to = fm.get("applyTo", "")
+            if isinstance(raw_apply_to, list):
+                apply_to = ",".join(str(item) for item in raw_apply_to)
+            else:
+                apply_to = str(raw_apply_to).strip()
+
+        safe_apply_to = apply_to.replace("\n", " ").replace("\r", " ").strip()
+        globs = parse_apply_to(safe_apply_to)
+
+        parts = ["---"]
+        if globs:
+            parts.append("inclusion: fileMatch")
+            if len(globs) == 1:
+                parts.append(f"fileMatchPattern: {yaml_double_quote(globs[0])}")
+            else:
+                parts.append("fileMatchPattern:")
+                parts.extend(f"  - {yaml_double_quote(g)}" for g in globs)
+        else:
+            parts.append("inclusion: always")
+        parts.append("---")
+
+        return "\n".join(parts) + "\n\n" + body.lstrip("\n")
+
     # ------------------------------------------------------------------
     # Claude Code Rules (.md with paths: frontmatter)
     # ------------------------------------------------------------------
diff --git a/src/apm_cli/integration/kiro_hook_integrator.py b/src/apm_cli/integration/kiro_hook_integrator.py
new file mode 100644
index 000000000..e1981de61
--- /dev/null
+++ b/src/apm_cli/integration/kiro_hook_integrator.py
@@ -0,0 +1,308 @@
+"""Kiro hook transformation helpers.
+
+Kiro stores each hook as its own JSON document under ``.kiro/hooks/``.
+This module keeps the target-specific expansion out of ``hook_integrator.py``
+so the shared integrator stays under the source-length guardrail.
+"""
+
+from __future__ import annotations
+
+import json
+import re
+import shutil
+from pathlib import Path
+
+from apm_cli.integration.hook_integrator import (
+    _HOOK_EVENT_MAP,
+    HookIntegrationResult,
+    _emit_hook_event_diagnostics,
+    _filter_hook_files_for_target,
+)
+from apm_cli.utils.path_security import ensure_path_within
+from apm_cli.utils.paths import portable_relpath
+
+_KIRO_EVENT_MAP = _HOOK_EVENT_MAP["kiro"]
+
+
+def _safe_hook_slug(value: str, fallback: str = "hook") -> str:
+    """Return a stable lowercase slug for generated Kiro hook filenames."""
+    safe = re.sub(r"[^A-Za-z0-9._-]+", "-", value.strip()).strip(".-_").lower()
+    return safe or fallback
+
+
+def _kiro_patterns_from_matcher(matcher: dict) -> list[str]:
+    """Extract Kiro file patterns from an APM hook matcher, if present."""
+    patterns = matcher.get("patterns")
+    if isinstance(patterns, str) and patterns.strip():
+        return [patterns.strip()]
+    if isinstance(patterns, list):
+        return [str(item).strip() for item in patterns if str(item).strip()]
+    matcher_value = matcher.get("matcher")
+    if isinstance(matcher_value, str) and matcher_value.strip():
+        return [matcher_value.strip()]
+    return []
+
+
+def _kiro_then_from_action(action: dict, command_keys: tuple[str, ...]) -> dict | None:
+    """Convert one APM hook action to Kiro's ``then`` object."""
+    prompt = action.get("prompt")
+    if action.get("type") == "askAgent" or isinstance(prompt, str):
+        prompt_text = prompt if isinstance(prompt, str) else action.get("command")
+        if isinstance(prompt_text, str) and prompt_text.strip():
+            return {"type": "askAgent", "prompt": prompt_text}
+        return None
+
+    for key in command_keys:
+        command = action.get(key)
+        if isinstance(command, str) and command.strip():
+            return {"type": "runCommand", "command": command}
+    return None
+
+
+def _kiro_actions_from_matcher(matcher: dict, command_keys: tuple[str, ...]) -> list[dict]:
+    """Return flat action dicts from both Copilot-flat and Claude-nested shapes."""
+    actions: list[dict] = []
+    if any(isinstance(matcher.get(key), str) for key in command_keys):
+        actions.append(matcher)
+    if isinstance(matcher.get("prompt"), str):
+        actions.append(matcher)
+    nested_hooks = matcher.get("hooks", [])
+    if isinstance(nested_hooks, list):
+        actions.extend(hook for hook in nested_hooks if isinstance(hook, dict))
+    return actions
+
+
+def _kiro_hook_document(
+    *,
+    name: str,
+    description: str | None,
+    event_name: str,
+    patterns: list[str],
+    then: dict,
+) -> dict:
+    """Build one Kiro hook JSON document."""
+    when: dict[str, object] = {"type": event_name}
+    if patterns:
+        when["patterns"] = patterns
+    doc = {
+        "name": name,
+        "version": "1.0.0",
+        "when": when,
+        "then": then,
+    }
+    if description:
+        doc["description"] = description
+    return doc
+
+
+def _write_kiro_hook_docs(
+    integrator,
+    hook_file: Path,
+    rewritten: dict,
+    hooks_dir: Path,
+    project_root: Path,
+    package_name: str,
+    force: bool,
+    managed_files: set | None,
+    diagnostics,
+    target_paths: list[Path],
+    display_payloads: list,
+) -> tuple[int, int, int]:
+    """Write Kiro hook docs from one source hook file."""
+    files_integrated = 0
+    files_skipped = 0
+    files_adopted = 0
+    hooks = rewritten.get("hooks", {})
+    _emit_hook_event_diagnostics(list(hooks.keys()), "kiro", _KIRO_EVENT_MAP)
+    description = rewritten.get("description")
+    if not isinstance(description, str) or not description.strip():
+        description = None
+
+    per_event_counts: dict[str, int] = {}
+    for raw_event_name, matchers in hooks.items():
+        if not isinstance(matchers, list):
+            continue
+        event_name = _KIRO_EVENT_MAP.get(raw_event_name, raw_event_name)
+        event_slug = _safe_hook_slug(event_name)
+        for matcher in matchers:
+            if not isinstance(matcher, dict):
+                continue
+            patterns = _kiro_patterns_from_matcher(matcher)
+            for action in _kiro_actions_from_matcher(matcher, integrator.HOOK_COMMAND_KEYS):
+                then = _kiro_then_from_action(action, integrator.HOOK_COMMAND_KEYS)
+                if then is None:
+                    continue
+                per_event_counts[event_name] = per_event_counts.get(event_name, 0) + 1
+                index = per_event_counts[event_name]
+                doc = _kiro_hook_document(
+                    name=f"{package_name} {event_name} {index}",
+                    description=description,
+                    event_name=event_name,
+                    patterns=patterns,
+                    then=then,
+                )
+                target_filename = (
+                    f"{_safe_hook_slug(package_name)}-{_safe_hook_slug(hook_file.stem)}-"
+                    f"{event_slug}-{index}.json"
+                )
+                target_path = hooks_dir / target_filename
+                ensure_path_within(target_path, hooks_dir)
+                rel_path = portable_relpath(target_path, project_root)
+                rendered = json.dumps(doc, indent=2) + "\n"
+
+                if target_path.exists() and target_path.read_text(encoding="utf-8") == rendered:
+                    files_adopted += 1
+                    target_paths.append(target_path)
+                    continue
+                if integrator.check_collision(
+                    target_path,
+                    rel_path,
+                    managed_files,
+                    force,
+                    diagnostics=diagnostics,
+                ):
+                    files_skipped += 1
+                    continue
+
+                target_path.write_text(rendered, encoding="utf-8")
+                files_integrated += 1
+                target_paths.append(target_path)
+                display_payloads.append(
+                    _display_payload(
+                        integrator, target_filename, hook_file, event_name, then, rendered
+                    )
+                )
+    return files_integrated, files_skipped, files_adopted
+
+
+def _display_payload(
+    integrator, target_filename: str, hook_file: Path, event_name: str, then: dict, rendered: str
+) -> dict:
+    """Build install-log metadata for one generated Kiro hook file."""
+    summary = (
+        integrator._summarize_command({"command": then.get("command", "")})
+        if then.get("type") == "runCommand"
+        else "asks agent"
+    )
+    return {
+        "target_label": ".kiro/hooks/",
+        "output_path": target_filename,
+        "source_hook_file": hook_file.name,
+        "actions": [{"event": event_name, "summary": summary}],
+        "rendered_json": rendered.rstrip("\n"),
+    }
+
+
+def _copy_scripts(
+    integrator,
+    scripts,
+    project_root: Path,
+    managed_files,
+    force: bool,
+    diagnostics,
+    target_paths: list[Path],
+) -> tuple[int, int]:
+    """Copy Kiro hook scripts and return copied/adopted counts."""
+    scripts_copied = 0
+    scripts_adopted = 0
+    for source_file, target_rel in scripts:
+        target_script = project_root / target_rel
+        ensure_path_within(target_script, project_root)
+        if integrator.try_adopt_identical(target_script, source_file, target_paths):
+            scripts_adopted += 1
+            continue
+        if integrator.check_collision(
+            target_script,
+            target_rel,
+            managed_files,
+            force,
+            diagnostics=diagnostics,
+        ):
+            continue
+        target_script.parent.mkdir(parents=True, exist_ok=True)
+        shutil.copy2(source_file, target_script)
+        scripts_copied += 1
+        target_paths.append(target_script)
+    return scripts_copied, scripts_adopted
+
+
+def integrate_kiro_hooks(
+    integrator,
+    package_info,
+    project_root: Path,
+    *,
+    force: bool = False,
+    managed_files: set | None = None,
+    diagnostics=None,
+    target=None,
+    user_scope: bool = False,
+) -> HookIntegrationResult:
+    """Integrate hooks as one Kiro JSON file per hook action."""
+    root_dir = target.root_dir if target else ".kiro"
+    target_dir = project_root / root_dir
+    if not target_dir.exists():
+        return HookIntegrationResult(0, 0, 0, [])
+
+    hook_files = integrator.find_hook_files(package_info.install_path)
+    hook_files = _filter_hook_files_for_target(hook_files, "kiro")
+    if not hook_files:
+        return HookIntegrationResult(0, 0, 0, [])
+
+    hooks_dir = target_dir / "hooks"
+    hooks_dir.mkdir(parents=True, exist_ok=True)
+    package_name = integrator._get_package_name(package_info, project_root)
+    deploy_root_for_rewrite = project_root if user_scope else None
+
+    files_integrated = 0
+    files_skipped = 0
+    files_adopted = 0
+    scripts_copied = 0
+    scripts_adopted = 0
+    target_paths: list[Path] = []
+    display_payloads: list = []
+
+    for hook_file in hook_files:
+        data = integrator._parse_hook_json(hook_file)
+        if data is None:
+            continue
+
+        rewritten, scripts = integrator._rewrite_hooks_data(
+            data,
+            package_info.install_path,
+            package_name,
+            "kiro",
+            hook_file_dir=hook_file.parent,
+            root_dir=root_dir,
+            deploy_root=deploy_root_for_rewrite,
+        )
+        written, skipped, adopted = _write_kiro_hook_docs(
+            integrator,
+            hook_file,
+            rewritten,
+            hooks_dir,
+            project_root,
+            package_name,
+            force,
+            managed_files,
+            diagnostics,
+            target_paths,
+            display_payloads,
+        )
+        files_integrated += written
+        files_skipped += skipped
+        files_adopted += adopted
+        copied, adopted_scripts = _copy_scripts(
+            integrator, scripts, project_root, managed_files, force, diagnostics, target_paths
+        )
+        scripts_copied += copied
+        scripts_adopted += adopted_scripts
+
+    return HookIntegrationResult(
+        files_integrated=files_integrated,
+        files_updated=0,
+        files_skipped=files_skipped,
+        target_paths=target_paths,
+        scripts_copied=scripts_copied,
+        files_adopted=files_adopted + scripts_adopted,
+        display_payloads=display_payloads,
+    )
diff --git a/src/apm_cli/integration/mcp_integrator.py b/src/apm_cli/integration/mcp_integrator.py
index 162fb9e07..994f34e7c 100644
--- a/src/apm_cli/integration/mcp_integrator.py
+++ b/src/apm_cli/integration/mcp_integrator.py
@@ -664,6 +664,24 @@ def remove_stale(
                 use_rich=True,
             )
 
+        if "kiro" in target_runtimes:
+            from apm_cli.factory import ClientFactory
+
+            kiro_cfg = Path(
+                ClientFactory.create_client(
+                    "kiro",
+                    project_root=project_root_path,
+                    user_scope=user_scope or scope is InstallScope.USER,
+                ).get_config_path()
+            )
+            _clean_json_mcp_config(
+                kiro_cfg,
+                expanded_stale,
+                logger,
+                "Kiro MCP config",
+                use_rich=True,
+            )
+
         # Clean JetBrains Copilot user-scope mcp.json
         if "intellij" in target_runtimes:
             from apm_cli.adapters.client.intellij import _intellij_config_dir
@@ -799,6 +817,8 @@ def _detect_runtimes(scripts: dict) -> list[str]:
                 detected.add("llm")
             if re.search(r"\bwindsurf\b", command):
                 detected.add("windsurf")
+            if re.search(r"\bkiro\b", command):
+                detected.add("kiro")
 
         return builtins.list(detected)
 
@@ -919,7 +939,7 @@ def _install_for_runtime(
         except ValueError as e:
             logger.warning(f"Runtime {runtime} not supported: {e}")
             logger.progress(
-                "Supported runtimes: vscode, copilot, codex, cursor, opencode, gemini, claude, windsurf, intellij, llm"
+                "Supported runtimes: vscode, copilot, codex, cursor, opencode, gemini, claude, windsurf, kiro, intellij, llm"
             )
             return False
         except Exception as e:
diff --git a/src/apm_cli/integration/mcp_integrator_install.py b/src/apm_cli/integration/mcp_integrator_install.py
index a7787044b..0b336e3a1 100644
--- a/src/apm_cli/integration/mcp_integrator_install.py
+++ b/src/apm_cli/integration/mcp_integrator_install.py
@@ -184,6 +184,53 @@ def _install_registry_group(
     return configured_count
 
 
+_MCP_RUNTIME_ORDER = (
+    "copilot",
+    "codex",
+    "vscode",
+    "cursor",
+    "opencode",
+    "gemini",
+    "windsurf",
+    "kiro",
+    "claude",
+    "intellij",
+)
+
+_DIRECTORY_OPT_IN_RUNTIMES = {
+    "cursor": ".cursor",
+    "opencode": ".opencode",
+    "gemini": ".gemini",
+    "windsurf": ".windsurf",
+    "kiro": ".kiro",
+}
+
+
+def _is_runtime_available_for_mcp(
+    runtime_name: str,
+    project_root_path: Path,
+    *,
+    user_scope: bool,
+    manager: Any,
+) -> bool:
+    """Return True when *runtime_name* should receive MCP config writes."""
+    from apm_cli.integration.mcp_integrator import _is_vscode_available
+
+    if runtime_name == "vscode":
+        return _is_vscode_available(project_root=project_root_path)
+    if runtime_name == "kiro" and user_scope:
+        return True
+    if runtime_name in _DIRECTORY_OPT_IN_RUNTIMES:
+        return (project_root_path / _DIRECTORY_OPT_IN_RUNTIMES[runtime_name]).is_dir()
+    if runtime_name == "claude":
+        return (project_root_path / ".claude").is_dir() or find_runtime_binary("claude") is not None
+    if runtime_name == "intellij":
+        from apm_cli.adapters.client.intellij import _intellij_config_dir
+
+        return _intellij_config_dir().is_dir()
+    return manager.is_runtime_available(runtime_name)
+
+
 def _resolve_target_runtimes(
     runtime: str | None,
     exclude: str | None,
@@ -202,10 +249,7 @@ def _resolve_target_runtimes(
     when the caller should immediately return 0 (e.g. all runtimes excluded,
     no user-scope-capable runtimes available).
     """
-    from apm_cli.integration.mcp_integrator import (
-        MCPIntegrator,
-        _is_vscode_available,
-    )
+    from apm_cli.integration.mcp_integrator import MCPIntegrator
 
     if runtime:
         # Single runtime mode - skip auto-discovery entirely.
@@ -232,72 +276,25 @@ def _resolve_target_runtimes(
             manager = RuntimeManager()
             installed_runtimes: list[str] = []
 
-            for runtime_name in [
-                "copilot",
-                "codex",
-                "vscode",
-                "cursor",
-                "opencode",
-                "gemini",
-                "windsurf",
-                "claude",
-                "intellij",
-            ]:
+            for runtime_name in _MCP_RUNTIME_ORDER:
                 try:
-                    if runtime_name == "vscode":
-                        if _is_vscode_available(project_root=project_root_path):
-                            ClientFactory.create_client(runtime_name)
-                            installed_runtimes.append(runtime_name)
-                    elif runtime_name == "cursor":
-                        # Cursor is opt-in: only target when .cursor/ exists
-                        if (project_root_path / ".cursor").is_dir():
-                            ClientFactory.create_client(runtime_name)
-                            installed_runtimes.append(runtime_name)
-                    elif runtime_name == "opencode":
-                        # OpenCode is opt-in: only target when .opencode/ exists
-                        if (project_root_path / ".opencode").is_dir():
-                            ClientFactory.create_client(runtime_name)
-                            installed_runtimes.append(runtime_name)
-                    elif runtime_name == "gemini":
-                        # Gemini CLI is opt-in: only target when .gemini/ exists
-                        if (project_root_path / ".gemini").is_dir():
-                            ClientFactory.create_client(runtime_name)
-                            installed_runtimes.append(runtime_name)
-                    elif runtime_name == "windsurf":
-                        # Windsurf is opt-in: only target when .windsurf/ exists
-                        if (project_root_path / ".windsurf").is_dir():
-                            ClientFactory.create_client(runtime_name)
-                            installed_runtimes.append(runtime_name)
-                    elif runtime_name == "claude":
-                        # Claude Code is opt-in: target when .claude/ exists
-                        # in the project (project-scope writes) OR when the
-                        # `claude` binary is on PATH (user-scope writes).
-                        # The PATH check is the gate that prevents the
-                        # adapter from writing to ~/.claude.json on hosts
-                        # where Claude Code was never installed.
-                        if (project_root_path / ".claude").is_dir() or (
-                            find_runtime_binary("claude") is not None
-                        ):
-                            ClientFactory.create_client(runtime_name)
-                            installed_runtimes.append(runtime_name)
-                    elif runtime_name == "intellij":
-                        # JetBrains Copilot is opt-in: target when the
-                        # user-scope config directory already exists.  This
-                        # directory is created by the JetBrains Copilot
-                        # plugin on first run, so its presence reliably
-                        # signals that the plugin is installed.
-                        from apm_cli.adapters.client.intellij import _intellij_config_dir
-
-                        if _intellij_config_dir().is_dir():
-                            ClientFactory.create_client(runtime_name)
-                            installed_runtimes.append(runtime_name)
-                    else:  # noqa: PLR5501
-                        if manager.is_runtime_available(runtime_name):
-                            ClientFactory.create_client(runtime_name)
-                            installed_runtimes.append(runtime_name)
+                    if _is_runtime_available_for_mcp(
+                        runtime_name,
+                        project_root_path,
+                        user_scope=user_scope,
+                        manager=manager,
+                    ):
+                        ClientFactory.create_client(
+                            runtime_name,
+                            project_root=project_root_path,
+                            user_scope=user_scope,
+                        )
+                        installed_runtimes.append(runtime_name)
                 except (ValueError, ImportError):
                     continue
         except ImportError:
+            from apm_cli.integration.mcp_integrator import _is_vscode_available
+
             installed_runtimes = [
                 rt for rt in ["copilot", "codex"] if find_runtime_binary(rt) is not None
             ]
@@ -316,6 +313,9 @@ def _resolve_target_runtimes(
             # Windsurf is directory-presence based
             if (project_root_path / ".windsurf").is_dir():
                 installed_runtimes.append("windsurf")
+            # Kiro is directory-presence based at project scope, always valid at user scope
+            if user_scope or (project_root_path / ".kiro").is_dir():
+                installed_runtimes.append("kiro")
             # Claude Code: directory-presence OR binary-on-PATH
             if (project_root_path / ".claude").is_dir() or (
                 find_runtime_binary("claude") is not None
diff --git a/src/apm_cli/integration/targets.py b/src/apm_cli/integration/targets.py
index b1b457fd0..bf6150d9b 100644
--- a/src/apm_cli/integration/targets.py
+++ b/src/apm_cli/integration/targets.py
@@ -21,7 +21,9 @@
 from collections.abc import Callable
 from dataclasses import dataclass
 
-RULE_FORMATS: frozenset[str] = frozenset({"cursor_rules", "claude_rules", "windsurf_rules"})
+RULE_FORMATS: frozenset[str] = frozenset(
+    {"cursor_rules", "claude_rules", "windsurf_rules", "kiro_steering"}
+)
 """Canonical set of format-transforming rule ``format_id``s.
 
 Single home for "which instruction formats transform their source on
@@ -64,7 +66,7 @@ class PrimitiveMapping:
     rendered *output* rather than the source bytes.
 
     This is the single source of truth for the rule-dir formats
-    (``cursor_rules``, ``claude_rules``, ``windsurf_rules``).  When ``True``:
+    (``cursor_rules``, ``claude_rules``, ``windsurf_rules``, ``kiro_steering``).  When ``True``:
 
     * The deployed file is never byte-identical to its source, so a
       source-based adopt always misses (apm#1662).  The integrator instead
@@ -554,6 +556,34 @@ def for_scope(self, user_scope: bool = False) -> TargetProfile | None:
         compile_family="agents",
         hooks_config_display=".cursor/hooks.json",
     ),
+    # Kiro IDE -- spec-driven development editor.
+    # Steering files use Kiro frontmatter under .kiro/steering/.
+    # Skills use the open Agent Skills SKILL.md layout under .kiro/skills/.
+    # Hooks are individual JSON files under .kiro/hooks/.
+    # MCP config lives at .kiro/settings/mcp.json and ~/.kiro/settings/mcp.json.
+    # Kiro CLI config divergence is intentionally out of scope for this v1 target.
+    # Ref: https://kiro.dev/docs/steering/
+    # Ref: https://kiro.dev/docs/skills/
+    # Ref: https://kiro.dev/docs/hooks/
+    "kiro": TargetProfile(
+        name="kiro",
+        root_dir=".kiro",
+        primitives={
+            "instructions": PrimitiveMapping(
+                "steering",
+                ".md",
+                "kiro_steering",
+                output_compare=True,
+            ),
+            "skills": PrimitiveMapping("skills", "/SKILL.md", "skill_standard"),
+            "hooks": PrimitiveMapping("hooks", ".json", "kiro_hooks"),
+        },
+        auto_create=False,
+        detect_by_dir=True,
+        user_supported=True,
+        user_root_dir=".kiro",
+        compile_family="agents",
+    ),
     # OpenCode -- at user scope, ~/.config/opencode/ supports skills, agents,
     # and commands.  OpenCode has no hooks concept, so "hooks" is excluded.
     "opencode": TargetProfile(
diff --git a/src/apm_cli/policy/policy_checks.py b/src/apm_cli/policy/policy_checks.py
index 2b9554fb3..dc566cebe 100644
--- a/src/apm_cli/policy/policy_checks.py
+++ b/src/apm_cli/policy/policy_checks.py
@@ -679,6 +679,7 @@ def _check_scripts_policy(
     ".cursor/rules",
     ".claude",
     ".opencode",
+    ".kiro",
 ]
 
 
diff --git a/tests/unit/core/test_scope.py b/tests/unit/core/test_scope.py
index 8b1158e71..59467c8a9 100644
--- a/tests/unit/core/test_scope.py
+++ b/tests/unit/core/test_scope.py
@@ -165,6 +165,7 @@ def test_all_known_targets_present(self):
             "codex",
             "gemini",
             "windsurf",
+            "kiro",
             "copilot-cowork",
             "copilot-app",
             "agent-skills",
@@ -194,6 +195,10 @@ def test_opencode_is_partially_supported(self):
         assert KNOWN_TARGETS["opencode"].user_root_dir == ".config/opencode"
         assert "hooks" in KNOWN_TARGETS["opencode"].unsupported_user_primitives
 
+    def test_kiro_is_supported_at_user_scope(self):
+        assert KNOWN_TARGETS["kiro"].user_supported is True
+        assert KNOWN_TARGETS["kiro"].user_root_dir == ".kiro"
+
     def test_copilot_user_root_dir(self):
         assert KNOWN_TARGETS["copilot"].user_root_dir == ".copilot"
 
diff --git a/tests/unit/core/test_target_detection.py b/tests/unit/core/test_target_detection.py
index e977efab4..1db065cd1 100644
--- a/tests/unit/core/test_target_detection.py
+++ b/tests/unit/core/test_target_detection.py
@@ -81,6 +81,16 @@ def test_explicit_target_all_wins(self, tmp_path):
         assert target == "all"
         assert reason == "explicit --target flag"
 
+    def test_explicit_target_kiro_wins(self, tmp_path):
+        """Explicit --target kiro always wins."""
+        target, reason = detect_target(
+            project_root=tmp_path,
+            explicit_target="kiro",
+        )
+
+        assert target == "kiro"
+        assert reason == "explicit --target flag"
+
     def test_config_target_copilot(self, tmp_path):
         """Config target copilot maps to vscode."""
         target, reason = detect_target(
@@ -165,6 +175,19 @@ def test_auto_detect_both_folders(self, tmp_path):
         assert target == "all"
         assert ".github/" in reason and ".claude/" in reason
 
+    def test_auto_detect_kiro_only(self, tmp_path):
+        """Auto-detect kiro when only .kiro/ exists."""
+        (tmp_path / ".kiro").mkdir()
+
+        target, reason = detect_target(
+            project_root=tmp_path,
+            explicit_target=None,
+            config_target=None,
+        )
+
+        assert target == "kiro"
+        assert "detected .kiro/ folder" in reason
+
     def test_auto_detect_neither_folder(self, tmp_path):
         """Auto-detect minimal when neither folder exists."""
         target, reason = detect_target(
diff --git a/tests/unit/core/test_target_resolution_v2.py b/tests/unit/core/test_target_resolution_v2.py
index 7f0cd42c0..19832638a 100644
--- a/tests/unit/core/test_target_resolution_v2.py
+++ b/tests/unit/core/test_target_resolution_v2.py
@@ -102,6 +102,11 @@ def test_signal_whitelist_gemini_md_is_signal(tmp_path):
     assert "gemini" in _signal_targets(tmp_path)
 
 
+def test_signal_whitelist_kiro_dir_is_signal(tmp_path):
+    (tmp_path / ".kiro").mkdir()
+    assert "kiro" in _signal_targets(tmp_path)
+
+
 def test_signal_whitelist_cursorrules_is_signal(tmp_path):
     _touch(tmp_path / ".cursorrules", "# Cursor\n")
     assert "cursor" in _signal_targets(tmp_path)
diff --git a/tests/unit/integration/test_data_driven_dispatch.py b/tests/unit/integration/test_data_driven_dispatch.py
index b728aaf61..f84bea922 100644
--- a/tests/unit/integration/test_data_driven_dispatch.py
+++ b/tests/unit/integration/test_data_driven_dispatch.py
@@ -318,6 +318,7 @@ def test_partition_parity_with_old_buckets(self):
             "commands_windsurf",
             "instructions",  # was instructions_copilot, aliased
             "instructions_windsurf",
+            "instructions_kiro",
             "rules_cursor",  # was instructions_cursor, aliased
             "rules_claude",  # was instructions_claude, aliased
             "skills",  # cross-target bucket
diff --git a/tests/unit/integration/test_kiro_target.py b/tests/unit/integration/test_kiro_target.py
new file mode 100644
index 000000000..ae30408f6
--- /dev/null
+++ b/tests/unit/integration/test_kiro_target.py
@@ -0,0 +1,199 @@
+"""Acceptance tests for the Kiro target profile and transforms (#702)."""
+
+from __future__ import annotations
+
+import json
+from datetime import datetime
+from pathlib import Path
+
+from apm_cli.integration.hook_integrator import HookIntegrator
+from apm_cli.integration.instruction_integrator import InstructionIntegrator
+from apm_cli.integration.targets import KNOWN_TARGETS
+from apm_cli.models.apm_package import APMPackage, GitReferenceType, PackageInfo, ResolvedReference
+
+
+def _make_package_info(package_dir: Path, name: str = "test-pkg") -> PackageInfo:
+    package = APMPackage(
+        name=name,
+        version="1.0.0",
+        package_path=package_dir,
+        source=f"github.com/test/{name}",
+    )
+    resolved_ref = ResolvedReference(
+        original_ref="main",
+        ref_type=GitReferenceType.BRANCH,
+        resolved_commit="abc123",
+        ref_name="main",
+    )
+    return PackageInfo(
+        package=package,
+        install_path=package_dir,
+        resolved_reference=resolved_ref,
+        installed_at=datetime.now().isoformat(),
+    )
+
+
+def test_kiro_target_profile_matches_ratified_layout() -> None:
+    target = KNOWN_TARGETS["kiro"]
+
+    assert target.root_dir == ".kiro"
+    assert target.auto_create is False
+    assert target.detect_by_dir is True
+    assert target.user_supported is True
+    assert target.user_root_dir == ".kiro"
+    assert set(target.primitives) == {"instructions", "skills", "hooks"}
+
+    instructions = target.primitives["instructions"]
+    assert instructions.subdir == "steering"
+    assert instructions.extension == ".md"
+    assert instructions.format_id == "kiro_steering"
+    assert instructions.output_compare is True
+
+    skills = target.primitives["skills"]
+    assert skills.subdir == "skills"
+    assert skills.extension == "/SKILL.md"
+    assert skills.format_id == "skill_standard"
+
+    hooks = target.primitives["hooks"]
+    assert hooks.subdir == "hooks"
+    assert hooks.extension == ".json"
+    assert hooks.format_id == "kiro_hooks"
+
+
+def test_kiro_steering_maps_apply_to_to_file_match(tmp_path: Path) -> None:
+    (tmp_path / ".kiro").mkdir()
+    package_dir = tmp_path / "pkg"
+    instructions_dir = package_dir / ".apm" / "instructions"
+    instructions_dir.mkdir(parents=True)
+    (instructions_dir / "python.instructions.md").write_text(
+        "---\n"
+        "description: Python rules\n"
+        'applyTo: "src/**/*.py,tests/**/*.py"\n'
+        "---\n\n"
+        "# Python\n\nUse type hints.\n",
+        encoding="utf-8",
+    )
+
+    result = InstructionIntegrator().integrate_instructions_for_target(
+        KNOWN_TARGETS["kiro"],
+        _make_package_info(package_dir),
+        tmp_path,
+    )
+
+    assert result.files_integrated == 1
+    target = tmp_path / ".kiro" / "steering" / "python.md"
+    assert target.exists()
+    assert target.read_text(encoding="utf-8") == (
+        "---\n"
+        "inclusion: fileMatch\n"
+        "fileMatchPattern:\n"
+        '  - "src/**/*.py"\n'
+        '  - "tests/**/*.py"\n'
+        "---\n\n"
+        "# Python\n\nUse type hints.\n"
+    )
+
+
+def test_kiro_steering_defaults_unscoped_instructions_to_always(tmp_path: Path) -> None:
+    (tmp_path / ".kiro").mkdir()
+    package_dir = tmp_path / "pkg"
+    instructions_dir = package_dir / ".apm" / "instructions"
+    instructions_dir.mkdir(parents=True)
+    (instructions_dir / "global.instructions.md").write_text(
+        "# Global\n\nUse this everywhere.\n",
+        encoding="utf-8",
+    )
+
+    result = InstructionIntegrator().integrate_instructions_for_target(
+        KNOWN_TARGETS["kiro"],
+        _make_package_info(package_dir),
+        tmp_path,
+    )
+
+    assert result.files_integrated == 1
+    target = tmp_path / ".kiro" / "steering" / "global.md"
+    assert target.read_text(encoding="utf-8") == (
+        "---\ninclusion: always\n---\n\n# Global\n\nUse this everywhere.\n"
+    )
+
+
+def test_kiro_hooks_expand_each_apm_hook_to_individual_json(tmp_path: Path) -> None:
+    (tmp_path / ".kiro").mkdir()
+    package_dir = tmp_path / "hookify"
+    hooks_dir = package_dir / "hooks"
+    hooks_dir.mkdir(parents=True)
+    hook_data = {
+        "description": "Validate before tool use",
+        "hooks": {
+            "PreToolUse": [
+                {
+                    "hooks": [
+                        {
+                            "type": "command",
+                            "command": "python ${PLUGIN_ROOT}/hooks/check.py",
+                        }
+                    ]
+                }
+            ],
+            "UserPromptSubmit": [
+                {
+                    "hooks": [
+                        {
+                            "type": "command",
+                            "command": "python ${PLUGIN_ROOT}/hooks/prompt.py",
+                        }
+                    ]
+                }
+            ],
+        },
+    }
+    (hooks_dir / "hooks.json").write_text(json.dumps(hook_data), encoding="utf-8")
+    (hooks_dir / "check.py").write_text("# check\n", encoding="utf-8")
+    (hooks_dir / "prompt.py").write_text("# prompt\n", encoding="utf-8")
+
+    result = HookIntegrator().integrate_hooks_for_target(
+        KNOWN_TARGETS["kiro"],
+        _make_package_info(package_dir, "hookify"),
+        tmp_path,
+    )
+
+    assert result.files_integrated == 2
+    assert result.scripts_copied == 2
+
+    pre_tool = tmp_path / ".kiro" / "hooks" / "hookify-hooks-pretooluse-1.json"
+    prompt_submit = tmp_path / ".kiro" / "hooks" / "hookify-hooks-promptsubmit-1.json"
+    assert pre_tool.exists()
+    assert prompt_submit.exists()
+
+    pre_data = json.loads(pre_tool.read_text(encoding="utf-8"))
+    assert pre_data["when"] == {"type": "preToolUse"}
+    assert pre_data["then"]["type"] == "runCommand"
+    assert pre_data["then"]["command"] == "python .kiro/hooks/hookify/hooks/check.py"
+    assert pre_data["description"] == "Validate before tool use"
+    assert "hooks" not in pre_data
+
+    prompt_data = json.loads(prompt_submit.read_text(encoding="utf-8"))
+    assert prompt_data["when"] == {"type": "promptSubmit"}
+    assert prompt_data["then"]["command"] == "python .kiro/hooks/hookify/hooks/prompt.py"
+
+    assert (tmp_path / ".kiro" / "hooks" / "hookify" / "hooks" / "check.py").exists()
+    assert (tmp_path / ".kiro" / "hooks" / "hookify" / "hooks" / "prompt.py").exists()
+
+
+def test_kiro_hooks_skip_when_project_has_no_kiro_dir(tmp_path: Path) -> None:
+    package_dir = tmp_path / "hookify"
+    hooks_dir = package_dir / "hooks"
+    hooks_dir.mkdir(parents=True)
+    (hooks_dir / "hooks.json").write_text(
+        json.dumps({"hooks": {"PreToolUse": [{"hooks": [{"command": "echo hi"}]}]}}),
+        encoding="utf-8",
+    )
+
+    result = HookIntegrator().integrate_hooks_for_target(
+        KNOWN_TARGETS["kiro"],
+        _make_package_info(package_dir, "hookify"),
+        tmp_path,
+    )
+
+    assert result.files_integrated == 0
+    assert not (tmp_path / ".kiro").exists()
diff --git a/tests/unit/integration/test_targets_registry_completeness.py b/tests/unit/integration/test_targets_registry_completeness.py
index 6ee5d2de4..822b0500a 100644
--- a/tests/unit/integration/test_targets_registry_completeness.py
+++ b/tests/unit/integration/test_targets_registry_completeness.py
@@ -22,6 +22,7 @@
 from apm_cli.adapters.client.cursor import CursorClientAdapter
 from apm_cli.adapters.client.gemini import GeminiClientAdapter
 from apm_cli.adapters.client.intellij import IntelliJClientAdapter
+from apm_cli.adapters.client.kiro import KiroClientAdapter
 from apm_cli.adapters.client.opencode import OpenCodeClientAdapter
 from apm_cli.adapters.client.vscode import VSCodeClientAdapter
 from apm_cli.adapters.client.windsurf import WindsurfClientAdapter
@@ -54,6 +55,7 @@
     CodexClientAdapter,
     GeminiClientAdapter,
     IntelliJClientAdapter,
+    KiroClientAdapter,
     OpenCodeClientAdapter,
     VSCodeClientAdapter,
     WindsurfClientAdapter,
diff --git a/tests/unit/test_kiro_mcp.py b/tests/unit/test_kiro_mcp.py
new file mode 100644
index 000000000..a7ae2f0e9
--- /dev/null
+++ b/tests/unit/test_kiro_mcp.py
@@ -0,0 +1,162 @@
+"""Acceptance tests for Kiro MCP adapter support (#702)."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+from unittest.mock import MagicMock, patch
+
+from apm_cli.adapters.client.kiro import KiroClientAdapter
+from apm_cli.factory import ClientFactory
+
+
+class TestKiroClientFactory:
+    """Verify KiroClientAdapter registration."""
+
+    def test_factory_creates_kiro_adapter(self) -> None:
+        adapter = ClientFactory.create_client("kiro")
+        assert isinstance(adapter, KiroClientAdapter)
+
+    def test_factory_accepts_case_insensitive_name(self) -> None:
+        adapter = ClientFactory.create_client("Kiro")
+        assert isinstance(adapter, KiroClientAdapter)
+
+
+class TestKiroClientAdapter:
+    """Core config operations for KiroClientAdapter."""
+
+    def test_project_config_path_uses_kiro_settings(self, tmp_path: Path) -> None:
+        adapter = KiroClientAdapter(project_root=tmp_path)
+        assert adapter.get_config_path() == str(tmp_path / ".kiro" / "settings" / "mcp.json")
+
+    def test_user_scope_config_path_uses_home_kiro_settings(self, tmp_path: Path) -> None:
+        with patch("pathlib.Path.home", return_value=tmp_path):
+            adapter = KiroClientAdapter(user_scope=True)
+            assert adapter.get_config_path() == str(tmp_path / ".kiro" / "settings" / "mcp.json")
+
+    def test_update_config_skips_project_without_kiro_dir(self, tmp_path: Path) -> None:
+        adapter = KiroClientAdapter(project_root=tmp_path)
+        adapter.update_config({"srv": {"command": "node"}})
+        assert not (tmp_path / ".kiro" / "settings" / "mcp.json").exists()
+
+    def test_update_config_creates_settings_inside_existing_kiro_dir(self, tmp_path: Path) -> None:
+        (tmp_path / ".kiro").mkdir()
+        adapter = KiroClientAdapter(project_root=tmp_path)
+
+        adapter.update_config({"srv": {"command": "node", "args": ["server.js"]}})
+
+        mcp_json = tmp_path / ".kiro" / "settings" / "mcp.json"
+        data = json.loads(mcp_json.read_text(encoding="utf-8"))
+        assert data["mcpServers"]["srv"]["command"] == "node"
+        assert data["mcpServers"]["srv"]["args"] == ["server.js"]
+
+    def test_user_scope_writes_without_existing_kiro_dir(self, tmp_path: Path) -> None:
+        with patch("pathlib.Path.home", return_value=tmp_path):
+            adapter = KiroClientAdapter(user_scope=True)
+            adapter.update_config({"srv": {"command": "node"}})
+
+        mcp_json = tmp_path / ".kiro" / "settings" / "mcp.json"
+        data = json.loads(mcp_json.read_text(encoding="utf-8"))
+        assert data["mcpServers"]["srv"]["command"] == "node"
+
+    def test_remote_config_uses_kiro_url_headers_and_tool_extensions(self) -> None:
+        adapter = KiroClientAdapter()
+        server_info = {
+            "id": "registry-id-is-not-kiro-config",
+            "name": "remote-server",
+            "remotes": [
+                {
+                    "transport_type": "http",
+                    "url": "https://mcp.example.com/server",
+                    "headers": [{"name": "Authorization", "value": "Bearer ${KIRO_TOKEN}"}],
+                }
+            ],
+            "autoApprove": ["search"],
+            "disabledTools": ["delete"],
+        }
+
+        with patch.dict("os.environ", {"KIRO_TOKEN": "literal-secret"}, clear=False):
+            config = adapter._format_server_config(server_info)
+
+        assert config == {
+            "url": "https://mcp.example.com/server",
+            "headers": {"Authorization": "Bearer ${KIRO_TOKEN}"},
+            "autoApprove": ["search"],
+            "disabledTools": ["delete"],
+        }
+        assert "literal-secret" not in json.dumps(config)
+        assert "type" not in config
+        assert "tools" not in config
+        assert "id" not in config
+
+    def test_stdio_env_literals_are_written_as_runtime_placeholders(self) -> None:
+        adapter = KiroClientAdapter()
+        server_info = {
+            "name": "stdio-server",
+            "_raw_stdio": {
+                "command": "node",
+                "args": ["server.js", "--token", "${KIRO_TOKEN}"],
+                "env": {"KIRO_TOKEN": "literal-secret"},
+            },
+        }
+
+        with patch.dict("os.environ", {"KIRO_TOKEN": "ignored-os-secret"}, clear=False):
+            config = adapter._format_server_config(server_info)
+
+        assert config["command"] == "node"
+        assert config["args"] == ["server.js", "--token", "${KIRO_TOKEN}"]
+        assert config["env"] == {"KIRO_TOKEN": "${KIRO_TOKEN}"}
+        assert "literal-secret" not in json.dumps(config)
+        assert "ignored-os-secret" not in json.dumps(config)
+
+    def test_configure_mcp_server_writes_project_config(self, tmp_path: Path) -> None:
+        (tmp_path / ".kiro").mkdir()
+        adapter = KiroClientAdapter(project_root=tmp_path)
+        adapter.registry_client = MagicMock()
+        adapter.registry_client.find_server_by_reference.return_value = {
+            "packages": [{"name": "pkg", "registry_name": "npm", "runtime_hint": "npx"}]
+        }
+
+        assert adapter.configure_mcp_server("scope/server", server_name="srv") is True
+
+        data = json.loads((tmp_path / ".kiro" / "settings" / "mcp.json").read_text())
+        assert data["mcpServers"]["srv"]["command"] == "npx"
+
+    def test_configure_mcp_server_sets_disabled_when_enabled_false(self, tmp_path: Path) -> None:
+        (tmp_path / ".kiro").mkdir()
+        adapter = KiroClientAdapter(project_root=tmp_path)
+        adapter.registry_client = MagicMock()
+        adapter.registry_client.find_server_by_reference.return_value = {
+            "packages": [{"name": "pkg", "registry_name": "npm", "runtime_hint": "npx"}]
+        }
+
+        assert (
+            adapter.configure_mcp_server("scope/server", server_name="srv", enabled=False) is True
+        )
+
+        data = json.loads((tmp_path / ".kiro" / "settings" / "mcp.json").read_text())
+        assert data["mcpServers"]["srv"]["disabled"] is True
+
+    def test_remove_stale_kiro_project_config(self, tmp_path: Path) -> None:
+        from apm_cli.integration.mcp_integrator import MCPIntegrator
+
+        settings_dir = tmp_path / ".kiro" / "settings"
+        settings_dir.mkdir(parents=True)
+        mcp_json = settings_dir / "mcp.json"
+        mcp_json.write_text(
+            json.dumps(
+                {
+                    "mcpServers": {
+                        "keep": {"command": "node"},
+                        "stale": {"command": "python"},
+                    }
+                }
+            ),
+            encoding="utf-8",
+        )
+
+        MCPIntegrator.remove_stale({"stale"}, runtime="kiro", project_root=tmp_path)
+
+        data = json.loads(mcp_json.read_text(encoding="utf-8"))
+        assert "keep" in data["mcpServers"]
+        assert "stale" not in data["mcpServers"]

From c12ef2c509c5628d8bfc95df851118c18d9f24bb Mon Sep 17 00:00:00 2001
From: danielmeppiel <danielmeppiel@users.noreply.github.com>
Date: Thu, 11 Jun 2026 01:11:30 +0200
Subject: [PATCH 2/9] Fold Kiro review follow-ups

Folds copilot-pull-request-reviewer follow-ups: correct Kiro hook filename pattern in docs, changelog PR number.

Refs #702

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 CHANGELOG.md                                                    | 2 +-
 .../docs/producer/author-primitives/hooks-and-commands.md       | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 908c67404..c0f92d975 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 
 - Kiro IDE is now a first-class install target: APM deploys steering, skills,
-  hooks, and MCP config to the documented `.kiro/` layout. (closes #702)
+  hooks, and MCP config to the documented `.kiro/` layout. (closes #702) (#1741)
 - `apm install --target hermes` and `apm compile -t hermes` add the Hermes
   agent (Nous Research) as a new experimental target (opt in via
   `apm experimental enable hermes`). Skills deploy to `.agents/skills/<name>/SKILL.md`
diff --git a/docs/src/content/docs/producer/author-primitives/hooks-and-commands.md b/docs/src/content/docs/producer/author-primitives/hooks-and-commands.md
index c052afaed..2a0f23cca 100644
--- a/docs/src/content/docs/producer/author-primitives/hooks-and-commands.md
+++ b/docs/src/content/docs/producer/author-primitives/hooks-and-commands.md
@@ -92,7 +92,7 @@ Supported targets and where the integrator writes:
 | gemini   | `.gemini/settings.json`               | merged               |
 | codex    | `.codex/hooks.json`                   | merged               |
 | windsurf | `.windsurf/hooks.json`                | merged               |
-| kiro     | `.kiro/hooks/<pkg>-<event>.json`    | one file per hook    |
+| kiro     | `.kiro/hooks/<package-slug>-<hook-file-stem-slug>-<event-slug>-<n>.json` | one file per hook action |
 | opencode | -- not supported --                   | silently skipped     |
 
 Copilot hook files are namespaced with the source package name to avoid

From acac4605d585727d3fcf2f08290b434f304c842c Mon Sep 17 00:00:00 2001
From: danielmeppiel <danielmeppiel@users.noreply.github.com>
Date: Thu, 11 Jun 2026 15:42:22 +0200
Subject: [PATCH 3/9] Fold Kiro panel follow-ups

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 CHANGELOG.md                                  |  4 +-
 .../docs/integrations/ide-tool-integration.md | 13 +++--
 docs/src/content/docs/producer/compile.md     | 13 +++--
 .../content/docs/reference/targets-matrix.md  |  2 +-
 .../skills/apm-usage/package-authoring.md     |  3 +-
 src/apm_cli/adapters/client/kiro.py           | 25 ++++----
 .../integration/kiro_hook_integrator.py       |  3 +
 src/apm_cli/integration/mcp_integrator.py     |  7 ++-
 tests/unit/integration/test_kiro_target.py    |  2 +
 tests/unit/test_kiro_mcp.py                   | 57 +++++++++++++++++++
 10 files changed, 99 insertions(+), 30 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c0f92d975..027aef9d5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,8 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 
-- Kiro IDE is now a first-class install target: APM deploys steering, skills,
-  hooks, and MCP config to the documented `.kiro/` layout. (closes #702) (#1741)
+- `apm install --target kiro` deploys Kiro IDE steering, skills, hooks,
+  and MCP config to the documented `.kiro/` layout. (closes #702) (#1741)
 - `apm install --target hermes` and `apm compile -t hermes` add the Hermes
   agent (Nous Research) as a new experimental target (opt in via
   `apm experimental enable hermes`). Skills deploy to `.agents/skills/<name>/SKILL.md`
diff --git a/docs/src/content/docs/integrations/ide-tool-integration.md b/docs/src/content/docs/integrations/ide-tool-integration.md
index 8110ae47c..a1ce53d5f 100644
--- a/docs/src/content/docs/integrations/ide-tool-integration.md
+++ b/docs/src/content/docs/integrations/ide-tool-integration.md
@@ -119,15 +119,16 @@ For server installation patterns, registry resolution, and trust model, see [MCP
 
 ### Kiro IDE
 
-Kiro reads project configuration from `.kiro/`. APM maps instructions to
-`.kiro/steering/` and converts `applyTo:` scoping into Kiro steering
-frontmatter (`inclusion: fileMatch`); unscoped instructions become
+[Kiro](https://kiro.dev) reads project configuration from `.kiro/`. APM maps
+instructions to `.kiro/steering/` and converts `applyTo:` scoping into Kiro
+steering frontmatter (`inclusion: fileMatch`); unscoped instructions become
 `inclusion: always`. Skills are copied verbatim to `.kiro/skills/`, hooks
-become one JSON file per hook in `.kiro/hooks/`, and MCP servers are written
-to `.kiro/settings/mcp.json` or `~/.kiro/settings/mcp.json` for `--global`.
+become one JSON file per hook action in `.kiro/hooks/`, and MCP servers are
+written to `.kiro/settings/mcp.json` or `~/.kiro/settings/mcp.json` for
+`--global`.
 
 This target covers the documented Kiro IDE layout. Kiro CLI configuration
-differences are tracked separately.
+differences are tracked separately; see [the targets matrix](../reference/targets-matrix/#kiro).
 
 ### JetBrains (IntelliJ IDEA, PyCharm, GoLand, and others)
 
diff --git a/docs/src/content/docs/producer/compile.md b/docs/src/content/docs/producer/compile.md
index e7a2b0323..1b530b0a8 100644
--- a/docs/src/content/docs/producer/compile.md
+++ b/docs/src/content/docs/producer/compile.md
@@ -20,8 +20,10 @@ Compile is **recommended for every other target** (`claude`,
 `cursor`, `codex`, `gemini`, `opencode`, `windsurf`, `kiro`) -- those
 harnesses load instructions through the root context file
 (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`) or a harness-specific rules
-folder that compile generates. Without it, your instructions are
-on disk but the harness will not pick them up.
+folder that compile generates. Kiro receives `.kiro/steering/` files;
+its `AGENTS.md` output remains the cross-harness fallback. Without
+compile, your instructions are on disk but the harness will not pick
+them up.
 :::
 
 ```bash
@@ -32,8 +34,9 @@ Concretely, that command rolls your `instructions/*.instructions.md`
 (see [Instructions](./author-primitives/instructions-and-agents/#1-instructions))
 into the native rules surface each target expects:
 
-- `AGENTS.md` -- the cross-harness root context file (Copilot, Codex,
-  OpenCode, Windsurf, and Kiro all read this).
+- `AGENTS.md` -- the cross-harness root context file. Copilot, Codex,
+  OpenCode, and Windsurf read it directly; Kiro primarily uses the
+  `.kiro/steering/` files that compile also emits.
 - `CLAUDE.md` -- Claude Code's root context file.
 - `GEMINI.md` -- Gemini CLI's root context file.
 - per-harness rules trees that mirror each instruction's
@@ -129,7 +132,7 @@ Per target, with the rules shape on disk after compile:
 | `gemini` | `GEMINI.md` (folded) | none -- compile-only, no per-file deploy | Yes -- folded into `GEMINI.md` |
 | `opencode` | `AGENTS.md` (folded) | none -- compile-only, no per-file deploy | Yes -- folded into `AGENTS.md` |
 | `windsurf` | -- | `.windsurf/rules/<name>.md` | Yes -- compiled to Windsurf rules |
-| `kiro` | `AGENTS.md` (folded) | `.kiro/steering/<name>.md` | Yes -- compiled to Kiro steering |
+| `kiro` | `AGENTS.md` (fallback) | `.kiro/steering/<name>.md` | Yes -- compiled to Kiro steering |
 
 ## compile vs install
 
diff --git a/docs/src/content/docs/reference/targets-matrix.md b/docs/src/content/docs/reference/targets-matrix.md
index c73a12a89..87207a1dc 100644
--- a/docs/src/content/docs/reference/targets-matrix.md
+++ b/docs/src/content/docs/reference/targets-matrix.md
@@ -184,7 +184,7 @@ Kiro IDE.
 - **File conventions.**
   - instructions: `.kiro/steering/<name>.md` with `inclusion: always` or `inclusion: fileMatch` frontmatter
   - skills: `.kiro/skills/<name>/SKILL.md`
-  - hooks: one JSON file per hook under `.kiro/hooks/`
+  - hooks: one JSON file per hook action under `.kiro/hooks/`
   - mcp: `.kiro/settings/mcp.json` (project) or `~/.kiro/settings/mcp.json` (user)
 - **MCP shape.** JSON `mcpServers` entries use `command`/`args`/`env` for stdio and `url`/`headers` for remote servers. Kiro resolves `${VAR}` placeholders at runtime, so APM preserves them rather than writing secrets to disk.
 - **Scope.** This is the documented Kiro IDE layout only. Kiro CLI differences are tracked separately and are not part of this target.
diff --git a/packages/apm-guide/.apm/skills/apm-usage/package-authoring.md b/packages/apm-guide/.apm/skills/apm-usage/package-authoring.md
index bee4f70aa..ccc17c95d 100644
--- a/packages/apm-guide/.apm/skills/apm-usage/package-authoring.md
+++ b/packages/apm-guide/.apm/skills/apm-usage/package-authoring.md
@@ -116,7 +116,8 @@ my-hooks-pkg/
 APM automatically normalises event names per target (e.g. `postToolUse`
 becomes `PostToolUse` in Claude) and rewrites path variables
 (`${PLUGIN_ROOT}`, `${CURSOR_PLUGIN_ROOT}`, `${CLAUDE_PLUGIN_ROOT}`) to
-the correct target-specific form.
+the correct target-specific form. Kiro materializes one JSON document per
+hook action under `.kiro/hooks/`.
 
 ### Hook command paths: project-scope stays repo-relative
 
diff --git a/src/apm_cli/adapters/client/kiro.py b/src/apm_cli/adapters/client/kiro.py
index 739177def..e15226841 100644
--- a/src/apm_cli/adapters/client/kiro.py
+++ b/src/apm_cli/adapters/client/kiro.py
@@ -78,17 +78,17 @@ def update_config(self, config_updates: dict[str, dict[str, Any]]) -> bool | Non
         return True
 
     @staticmethod
-    def _header_mapping(remote: dict[str, Any]) -> dict[str, Any]:
-        """Return registry remote headers as a dict."""
+    def _header_mapping(remote: dict[str, Any]) -> dict[str, str]:
+        """Return registry remote headers as string key-value pairs."""
         headers = remote.get("headers", {})
         if isinstance(headers, list):
             return {
-                h["name"]: h["value"]
+                str(h["name"]): str(h["value"])
                 for h in headers
                 if isinstance(h, dict) and "name" in h and "value" in h
             }
         if isinstance(headers, dict):
-            return dict(headers)
+            return {str(name): str(value) for name, value in headers.items()}
         return {}
 
     @staticmethod
@@ -179,12 +179,12 @@ def _format_server_config(
 
     def configure_mcp_server(
         self,
-        server_url,
-        server_name=None,
-        enabled=True,
-        env_overrides=None,
-        server_info_cache=None,
-        runtime_vars=None,
+        server_url: str,
+        server_name: str | None = None,
+        enabled: bool = True,
+        env_overrides: dict[str, str] | None = None,
+        server_info_cache: dict[str, Any] | None = None,
+        runtime_vars: dict[str, str] | None = None,
     ) -> bool:
         """Configure an MCP server in Kiro's MCP config."""
         if not server_url:
@@ -198,6 +198,8 @@ def configure_mcp_server(
             )
             return True
 
+        config_key = self._determine_config_key(server_url, server_name)
+
         try:
             server_info = self._fetch_server_info(server_url, server_info_cache)
             if server_info is None:
@@ -206,7 +208,6 @@ def configure_mcp_server(
             self._last_env_placeholder_keys = set()
             self._last_legacy_angle_vars = set()
 
-            config_key = self._determine_config_key(server_url, server_name)
             server_config = self._format_server_config(server_info, env_overrides, runtime_vars)
             if not enabled:
                 server_config["disabled"] = True
@@ -216,5 +217,5 @@ def configure_mcp_server(
             return True
         except Exception as exc:
             logger.debug("Kiro MCP configuration failed: %s", exc)
-            _rich_error("Failed to configure MCP server for Kiro", symbol="error")
+            _rich_error(f"Failed to configure MCP server '{config_key}' for Kiro", symbol="error")
             return False
diff --git a/src/apm_cli/integration/kiro_hook_integrator.py b/src/apm_cli/integration/kiro_hook_integrator.py
index e1981de61..c5bbe4c05 100644
--- a/src/apm_cli/integration/kiro_hook_integrator.py
+++ b/src/apm_cli/integration/kiro_hook_integrator.py
@@ -8,6 +8,7 @@
 from __future__ import annotations
 
 import json
+import os
 import re
 import shutil
 from pathlib import Path
@@ -151,6 +152,7 @@ def _write_kiro_hook_docs(
                 rendered = json.dumps(doc, indent=2) + "\n"
 
                 if target_path.exists() and target_path.read_text(encoding="utf-8") == rendered:
+                    os.chmod(target_path, 0o600)
                     files_adopted += 1
                     target_paths.append(target_path)
                     continue
@@ -165,6 +167,7 @@ def _write_kiro_hook_docs(
                     continue
 
                 target_path.write_text(rendered, encoding="utf-8")
+                os.chmod(target_path, 0o600)
                 files_integrated += 1
                 target_paths.append(target_path)
                 display_payloads.append(
diff --git a/src/apm_cli/integration/mcp_integrator.py b/src/apm_cli/integration/mcp_integrator.py
index 994f34e7c..8987a7d95 100644
--- a/src/apm_cli/integration/mcp_integrator.py
+++ b/src/apm_cli/integration/mcp_integrator.py
@@ -937,10 +937,11 @@ def _install_for_runtime(
             logger.progress(f"Dependencies for {runtime}: {', '.join(mcp_deps)}")
             return False
         except ValueError as e:
+            from apm_cli.factory import ClientFactory
+
+            supported_runtimes = ", ".join(sorted(ClientFactory.supported_clients()))
             logger.warning(f"Runtime {runtime} not supported: {e}")
-            logger.progress(
-                "Supported runtimes: vscode, copilot, codex, cursor, opencode, gemini, claude, windsurf, kiro, intellij, llm"
-            )
+            logger.progress(f"Supported runtimes: {supported_runtimes}")
             return False
         except Exception as e:
             _log.debug("Unexpected error installing for runtime %s", runtime, exc_info=True)
diff --git a/tests/unit/integration/test_kiro_target.py b/tests/unit/integration/test_kiro_target.py
index ae30408f6..8f76fccbd 100644
--- a/tests/unit/integration/test_kiro_target.py
+++ b/tests/unit/integration/test_kiro_target.py
@@ -171,10 +171,12 @@ def test_kiro_hooks_expand_each_apm_hook_to_individual_json(tmp_path: Path) -> N
     assert pre_data["then"]["command"] == "python .kiro/hooks/hookify/hooks/check.py"
     assert pre_data["description"] == "Validate before tool use"
     assert "hooks" not in pre_data
+    assert pre_tool.stat().st_mode & 0o777 == 0o600
 
     prompt_data = json.loads(prompt_submit.read_text(encoding="utf-8"))
     assert prompt_data["when"] == {"type": "promptSubmit"}
     assert prompt_data["then"]["command"] == "python .kiro/hooks/hookify/hooks/prompt.py"
+    assert prompt_submit.stat().st_mode & 0o777 == 0o600
 
     assert (tmp_path / ".kiro" / "hooks" / "hookify" / "hooks" / "check.py").exists()
     assert (tmp_path / ".kiro" / "hooks" / "hookify" / "hooks" / "prompt.py").exists()
diff --git a/tests/unit/test_kiro_mcp.py b/tests/unit/test_kiro_mcp.py
index a7ae2f0e9..c70496bea 100644
--- a/tests/unit/test_kiro_mcp.py
+++ b/tests/unit/test_kiro_mcp.py
@@ -89,6 +89,46 @@ def test_remote_config_uses_kiro_url_headers_and_tool_extensions(self) -> None:
         assert "tools" not in config
         assert "id" not in config
 
+    def test_remote_header_values_are_string_coerced(self) -> None:
+        adapter = KiroClientAdapter()
+        server_info = {
+            "name": "remote-server",
+            "remotes": [
+                {
+                    "transport_type": "http",
+                    "url": "https://mcp.example.com/server",
+                    "headers": [{"name": "X-Retry", "value": 3}],
+                }
+            ],
+        }
+
+        config = adapter._format_server_config(server_info)
+
+        assert config["headers"] == {"X-Retry": "3"}
+
+    def test_remote_unsupported_transport_raises_for_kiro(self) -> None:
+        adapter = KiroClientAdapter()
+        server_info = {
+            "name": "remote-server",
+            "remotes": [
+                {
+                    "transport_type": "websocket",
+                    "url": "https://mcp.example.com/server",
+                }
+            ],
+        }
+
+        try:
+            adapter._format_server_config(server_info)
+        except ValueError as exc:
+            message = str(exc)
+        else:
+            raise AssertionError("Expected ValueError for unsupported Kiro transport")
+
+        assert "Unsupported remote transport" in message
+        assert "websocket" in message
+        assert "Kiro" in message
+
     def test_stdio_env_literals_are_written_as_runtime_placeholders(self) -> None:
         adapter = KiroClientAdapter()
         server_info = {
@@ -122,6 +162,23 @@ def test_configure_mcp_server_writes_project_config(self, tmp_path: Path) -> Non
         data = json.loads((tmp_path / ".kiro" / "settings" / "mcp.json").read_text())
         assert data["mcpServers"]["srv"]["command"] == "npx"
 
+    def test_configure_mcp_server_error_names_config_key(self, tmp_path: Path) -> None:
+        (tmp_path / ".kiro").mkdir()
+        adapter = KiroClientAdapter(project_root=tmp_path)
+        adapter.registry_client = MagicMock()
+        adapter.registry_client.find_server_by_reference.return_value = {
+            "name": "broken-server",
+            "remotes": [{"transport_type": "websocket", "url": "https://mcp.example.com"}],
+        }
+
+        with patch("apm_cli.adapters.client.kiro._rich_error") as rich_error:
+            assert adapter.configure_mcp_server("scope/server", server_name="srv") is False
+
+        rich_error.assert_called_once_with(
+            "Failed to configure MCP server 'srv' for Kiro",
+            symbol="error",
+        )
+
     def test_configure_mcp_server_sets_disabled_when_enabled_false(self, tmp_path: Path) -> None:
         (tmp_path / ".kiro").mkdir()
         adapter = KiroClientAdapter(project_root=tmp_path)

From 1a9614258d5e0e53195fc26367dd59d2d7e98a58 Mon Sep 17 00:00:00 2001
From: danielmeppiel <danielmeppiel@users.noreply.github.com>
Date: Thu, 11 Jun 2026 15:53:43 +0200
Subject: [PATCH 4/9] Clear remaining Kiro panel nits

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 README.md                                     |  6 +--
 .../docs/integrations/ide-tool-integration.md |  2 +-
 .../docs/producer/author-primitives/skills.md |  2 +-
 .../src/content/docs/reference/cli/install.md |  2 +-
 .../content/docs/reference/cli/uninstall.md   |  2 +-
 src/apm_cli/core/target_detection.py          |  2 +-
 .../integration/kiro_hook_integrator.py       | 17 ++++++--
 tests/unit/core/test_target_detection.py      |  8 ++++
 tests/unit/integration/test_kiro_target.py    | 42 +++++++++++++++++--
 9 files changed, 67 insertions(+), 16 deletions(-)

diff --git a/README.md b/README.md
index 595f9315a..423089383 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
 
 Think `package.json`, `requirements.txt`, or `Cargo.toml` — but for AI agent configuration.
 
-GitHub Copilot · Claude Code · Cursor · OpenCode · Codex · Gemini · Windsurf
+GitHub Copilot · Claude Code · Cursor · OpenCode · Codex · Gemini · Windsurf · Kiro
 
 **[Documentation](https://microsoft.github.io/apm/)** · **[Quick Start](https://microsoft.github.io/apm/getting-started/quick-start/)** · **[CLI Reference](https://microsoft.github.io/apm/reference/cli-commands/)** · **[Roadmap](https://github.com/orgs/microsoft/projects/2304)**
 
@@ -67,7 +67,7 @@ One command, no configuration -- VS Code and GitHub Copilot read the file automa
 
 One `apm.yml` describes every primitive your agents need — instructions, skills, prompts, agents, hooks, plugins, MCP servers — and `apm install` reproduces the exact same setup across every client on every machine. `apm.lock.yaml` pins the resolved tree the way `package-lock.json` does for npm.
 
-- **[One manifest for everything](https://microsoft.github.io/apm/reference/primitive-types/)** — declared once, deployed across Copilot, Claude, Cursor, OpenCode, Codex, Gemini, Windsurf
+- **[One manifest for everything](https://microsoft.github.io/apm/reference/primitive-types/)** — declared once, deployed across Copilot, Claude, Cursor, OpenCode, Codex, Gemini, Windsurf, Kiro
 - **[Install from anywhere](https://microsoft.github.io/apm/guides/dependencies/)** — GitHub, GitLab, Bitbucket, Azure DevOps, GitHub Enterprise, Gitea, Gogs, any git host
 - **[Transitive dependencies](https://microsoft.github.io/apm/guides/dependencies/)** — packages can depend on packages; APM resolves the full tree
 - **[Author plugins](https://microsoft.github.io/apm/guides/plugins/)** — build Copilot, Claude, and Cursor plugins with dependency management, then export standard `plugin.json`
@@ -146,7 +146,7 @@ apm marketplace add github/awesome-copilot
 apm install azure-cloud-development@awesome-copilot
 ```
 
-Or add an MCP server (wired into Copilot, Claude, Cursor, Codex, OpenCode, Gemini, and Windsurf):
+Or add an MCP server (wired into Copilot, Claude, Cursor, Codex, OpenCode, Gemini, Windsurf, and Kiro):
 
 ```bash
 apm install --mcp io.github.github/github-mcp-server --transport http   # connects over HTTPS
diff --git a/docs/src/content/docs/integrations/ide-tool-integration.md b/docs/src/content/docs/integrations/ide-tool-integration.md
index a1ce53d5f..295652610 100644
--- a/docs/src/content/docs/integrations/ide-tool-integration.md
+++ b/docs/src/content/docs/integrations/ide-tool-integration.md
@@ -60,7 +60,7 @@ Each primitive type maps to a target-specific slot:
 .apm/prompts/        ->   per target: prompt files / commands
 .apm/agents/         ->   per target: agent definitions (or skill conversion)
 .apm/skills/         ->   per target: skills directory (Claude, Codex, OpenCode, .agents)
-.apm/hooks/          ->   per target: lifecycle hooks / tool hooks
+.apm/hooks/          ->   per target: lifecycle hooks / tool hooks (see [Targets matrix](../reference/targets-matrix/))
 mcp: in apm.yml      ->   per target: .mcp.json / settings.json / equivalent
 ```
 
diff --git a/docs/src/content/docs/producer/author-primitives/skills.md b/docs/src/content/docs/producer/author-primitives/skills.md
index f0d604037..d159e94c1 100644
--- a/docs/src/content/docs/producer/author-primitives/skills.md
+++ b/docs/src/content/docs/producer/author-primitives/skills.md
@@ -111,7 +111,7 @@ in `references/`; keep `SKILL.md` to the always-relevant flow.
 Five harnesses converge on the cross-tool `.agents/skills/`
 directory. Claude keeps its harness-native path because Claude Code's
 default scan is `.claude/skills/`; Windsurf and Kiro currently use
-`.windsurf/skills/` and `.kiro/skills/` for the same reason, though Cascade also
+`.windsurf/skills/` and `.kiro/skills/` for the same reason. Windsurf's Cascade also
 [discovers `.agents/skills/`](https://docs.windsurf.com/windsurf/cascade/skills#skill-scopes)
 natively for cross-agent compatibility (convergence tracked in
 [#1520](https://github.com/microsoft/apm/issues/1520)). The whole
diff --git a/docs/src/content/docs/reference/cli/install.md b/docs/src/content/docs/reference/cli/install.md
index a953e9a90..8f5e52b49 100644
--- a/docs/src/content/docs/reference/cli/install.md
+++ b/docs/src/content/docs/reference/cli/install.md
@@ -199,7 +199,7 @@ apm install owner/skill-bundle --skill '*'   # reset to all skills
 
 - **`--force` is dual-purpose.** It overwrites locally-authored files on collision **and** disables the critical-finding block from the built-in security scan. It does **not** suppress general install errors -- any error reported in the diagnostic summary still exits `1` (matches `npm` / `pip` / `cargo`). It does **not** refresh remote refs -- for routine ref updates, run [`apm update`](../update/). To remediate findings, prefer `apm audit --strip`. See [Drift and secure by default](../../../consumer/drift-and-secure-by-default/).
 - **Claude target prompt rewrite.** When deploying to `.claude/commands/`, prompt files with an `input:` front-matter key are rewritten to Claude's `arguments:` shape and `${input:name}` placeholders become `$name`. Argument names must match `^[A-Za-z][\w-]{0,63}$`; rejected names are dropped with a warning.
-- **MCP env-var passthrough.** Copilot CLI and Kiro translate `${env:VAR}` and `<VAR>` to `${VAR}` in their MCP configs. JetBrains Copilot preserves env references as `${env:VAR}` in `github-copilot/intellij/mcp.json`. Plaintext secrets are never written to disk for these runtime-resolved targets; legacy targets resolve placeholders at install time.
+- **MCP env-var passthrough.** Copilot CLI and Kiro translate `${env:VAR}` and `<VAR>` to `${VAR}` in their MCP configs. Kiro writes `.kiro/settings/mcp.json` and `~/.kiro/settings/mcp.json` with `0o600` permissions. JetBrains Copilot preserves env references as `${env:VAR}` in `github-copilot/intellij/mcp.json`. Plaintext secrets are never written to disk for these runtime-resolved targets; legacy targets resolve placeholders at install time.
 
 ### Install from a private registry (experimental)
 
diff --git a/docs/src/content/docs/reference/cli/uninstall.md b/docs/src/content/docs/reference/cli/uninstall.md
index 88579440b..080346a0a 100644
--- a/docs/src/content/docs/reference/cli/uninstall.md
+++ b/docs/src/content/docs/reference/cli/uninstall.md
@@ -79,7 +79,7 @@ What gets removed, in order:
 2. The package folder under `apm_modules/owner/repo/`.
 3. Transitive dependencies that no remaining package depends on (npm-style pruning, computed from `apm.lock.yaml`).
 4. Every file in the lockfile's `deployed_files` for the removed packages and pruned orphans, across all harness folders (`.github/`, `.claude/`, `.cursor/`, `.opencode/`, `.gemini/`, `.codex/`, `.windsurf/`, `.kiro/`).
-5. Hook entries inside `.claude/settings.json`, `.cursor/hooks.json`, and `.gemini/settings.json` that the removed packages contributed.
+5. Hook entries inside `.claude/settings.json`, `.cursor/hooks.json`, `.gemini/settings.json`, and `.kiro/hooks/` that the removed packages contributed.
 6. MCP servers contributed only by the removed packages.
 7. The lockfile entries themselves. If no dependencies remain, `apm.lock.yaml` is deleted.
 8. Empty parent directories left behind by the cleanup.
diff --git a/src/apm_cli/core/target_detection.py b/src/apm_cli/core/target_detection.py
index f34b542f8..0b8a5415e 100644
--- a/src/apm_cli/core/target_detection.py
+++ b/src/apm_cli/core/target_detection.py
@@ -349,7 +349,7 @@ def get_target_description(target: UserTargetType) -> str:
         "codex": "AGENTS.md + .agents/skills/ + .codex/agents/ + .codex/hooks.json",
         "gemini": "GEMINI.md + .gemini/commands/ + .gemini/skills/ + .gemini/settings.json (MCP/hooks)",
         "windsurf": "AGENTS.md + .windsurf/rules/ + .windsurf/skills/ + .windsurf/workflows/ + .windsurf/hooks.json",
-        "kiro": "AGENTS.md + .kiro/steering/ + .kiro/skills/ + .kiro/hooks/",
+        "kiro": "AGENTS.md + .kiro/steering/ + .kiro/skills/ + .kiro/hooks/ + .kiro/settings/mcp.json",
         "agent-skills": ".agents/skills/ only (cross-client shared skills -- no agents, hooks, or commands)",
         "openclaw": ".agents/skills/ (project) or ~/.openclaw/skills/ (--global) -- experimental",
         "hermes": "AGENTS.md + .agents/skills/ (project) or ~/.hermes/skills/ + config.yaml MCP (--global) -- experimental",
diff --git a/src/apm_cli/integration/kiro_hook_integrator.py b/src/apm_cli/integration/kiro_hook_integrator.py
index c5bbe4c05..d96a2e379 100644
--- a/src/apm_cli/integration/kiro_hook_integrator.py
+++ b/src/apm_cli/integration/kiro_hook_integrator.py
@@ -12,6 +12,7 @@
 import re
 import shutil
 from pathlib import Path
+from typing import TYPE_CHECKING
 
 from apm_cli.integration.hook_integrator import (
     _HOOK_EVENT_MAP,
@@ -22,6 +23,9 @@
 from apm_cli.utils.path_security import ensure_path_within
 from apm_cli.utils.paths import portable_relpath
 
+if TYPE_CHECKING:
+    from apm_cli.integration.hook_integrator import HookIntegrator
+
 _KIRO_EVENT_MAP = _HOOK_EVENT_MAP["kiro"]
 
 
@@ -97,7 +101,7 @@ def _kiro_hook_document(
 
 
 def _write_kiro_hook_docs(
-    integrator,
+    integrator: HookIntegrator,
     hook_file: Path,
     rewritten: dict,
     hooks_dir: Path,
@@ -179,7 +183,12 @@ def _write_kiro_hook_docs(
 
 
 def _display_payload(
-    integrator, target_filename: str, hook_file: Path, event_name: str, then: dict, rendered: str
+    integrator: HookIntegrator,
+    target_filename: str,
+    hook_file: Path,
+    event_name: str,
+    then: dict,
+    rendered: str,
 ) -> dict:
     """Build install-log metadata for one generated Kiro hook file."""
     summary = (
@@ -197,7 +206,7 @@ def _display_payload(
 
 
 def _copy_scripts(
-    integrator,
+    integrator: HookIntegrator,
     scripts,
     project_root: Path,
     managed_files,
@@ -230,7 +239,7 @@ def _copy_scripts(
 
 
 def integrate_kiro_hooks(
-    integrator,
+    integrator: HookIntegrator,
     package_info,
     project_root: Path,
     *,
diff --git a/tests/unit/core/test_target_detection.py b/tests/unit/core/test_target_detection.py
index b1ef73df9..cbb91becb 100644
--- a/tests/unit/core/test_target_detection.py
+++ b/tests/unit/core/test_target_detection.py
@@ -351,6 +351,14 @@ def test_opencode_description(self):
         assert "AGENTS.md" in desc
         assert ".opencode/" in desc
 
+    def test_kiro_description_includes_mcp_config_path(self):
+        """Description for kiro target names its MCP config path."""
+        desc = get_target_description("kiro")
+        assert ".kiro/steering/" in desc
+        assert ".kiro/skills/" in desc
+        assert ".kiro/hooks/" in desc
+        assert ".kiro/settings/mcp.json" in desc
+
 
 class TestDetectTargetCursor:
     """Tests for auto-detection and explicit cursor target."""
diff --git a/tests/unit/integration/test_kiro_target.py b/tests/unit/integration/test_kiro_target.py
index 8f76fccbd..76ff8ce74 100644
--- a/tests/unit/integration/test_kiro_target.py
+++ b/tests/unit/integration/test_kiro_target.py
@@ -8,11 +8,22 @@
 
 from apm_cli.integration.hook_integrator import HookIntegrator
 from apm_cli.integration.instruction_integrator import InstructionIntegrator
+from apm_cli.integration.skill_integrator import SkillIntegrator
 from apm_cli.integration.targets import KNOWN_TARGETS
-from apm_cli.models.apm_package import APMPackage, GitReferenceType, PackageInfo, ResolvedReference
-
-
-def _make_package_info(package_dir: Path, name: str = "test-pkg") -> PackageInfo:
+from apm_cli.models.apm_package import (
+    APMPackage,
+    GitReferenceType,
+    PackageInfo,
+    PackageType,
+    ResolvedReference,
+)
+
+
+def _make_package_info(
+    package_dir: Path,
+    name: str = "test-pkg",
+    package_type: PackageType | None = None,
+) -> PackageInfo:
     package = APMPackage(
         name=name,
         version="1.0.0",
@@ -30,6 +41,7 @@ def _make_package_info(package_dir: Path, name: str = "test-pkg") -> PackageInfo
         install_path=package_dir,
         resolved_reference=resolved_ref,
         installed_at=datetime.now().isoformat(),
+        package_type=package_type,
     )
 
 
@@ -117,6 +129,28 @@ def test_kiro_steering_defaults_unscoped_instructions_to_always(tmp_path: Path)
     )
 
 
+def test_kiro_skills_deploy_skill_md_to_kiro_skills_dir(tmp_path: Path) -> None:
+    (tmp_path / ".kiro").mkdir()
+    package_dir = tmp_path / "skill-pkg"
+    package_dir.mkdir()
+    (package_dir / "SKILL.md").write_text(
+        "---\nname: skill-pkg\ndescription: Demo skill\n---\n\n# Demo\n",
+        encoding="utf-8",
+    )
+
+    result = SkillIntegrator().integrate_package_skill(
+        _make_package_info(package_dir, "skill-pkg", PackageType.CLAUDE_SKILL),
+        tmp_path,
+        targets=[KNOWN_TARGETS["kiro"]],
+    )
+
+    target = tmp_path / ".kiro" / "skills" / "skill-pkg" / "SKILL.md"
+    assert result.skill_created is True
+    assert target.read_text(encoding="utf-8") == (
+        "---\nname: skill-pkg\ndescription: Demo skill\n---\n\n# Demo\n"
+    )
+
+
 def test_kiro_hooks_expand_each_apm_hook_to_individual_json(tmp_path: Path) -> None:
     (tmp_path / ".kiro").mkdir()
     package_dir = tmp_path / "hookify"

From 0c49978e89ca65d2f0008dc21aa3dfa94662f654 Mon Sep 17 00:00:00 2001
From: danielmeppiel <danielmeppiel@users.noreply.github.com>
Date: Thu, 11 Jun 2026 16:20:48 +0200
Subject: [PATCH 5/9] fix(kiro): atomic+0o600 config/hook writes, kiro in
 compile/install help

Fold remaining apm-review-panel follow-ups for the Kiro target: write the
.kiro MCP config and per-hook docs via atomic_write_text with 0o600 (no
torn-write window), list `kiro` in `apm compile --target` and `apm install
--runtime`/global-scope help, and note .kiro in the lifecycle compile doc.
Adds focused regression tests for AGENTS.md kiro fallback and the install
help surface.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 docs/src/content/docs/concepts/lifecycle.md     | 4 ++--
 src/apm_cli/adapters/client/kiro.py             | 8 ++++++--
 src/apm_cli/commands/install.py                 | 4 ++--
 src/apm_cli/integration/kiro_hook_integrator.py | 3 ++-
 tests/unit/core/test_target_detection.py        | 4 ++++
 tests/unit/install/test_no_policy_flag.py       | 7 +++++++
 6 files changed, 23 insertions(+), 7 deletions(-)

diff --git a/docs/src/content/docs/concepts/lifecycle.md b/docs/src/content/docs/concepts/lifecycle.md
index 506866094..9bb6f8df3 100644
--- a/docs/src/content/docs/concepts/lifecycle.md
+++ b/docs/src/content/docs/concepts/lifecycle.md
@@ -74,11 +74,11 @@ Order of operations is deterministic and worth memorizing:
 apm compile [--target <list>]
 ```
 
-Transforms the primitives in `.apm/` (and dependencies under `apm_modules/`) into harness-native files: `AGENTS.md` for Codex, `GEMINI.md` for Gemini, populated `.cursor/`, `.opencode/`, `.windsurf/` directories, and so on.
+Transforms the primitives in `.apm/` (and dependencies under `apm_modules/`) into harness-native files: `AGENTS.md` for Codex, `GEMINI.md` for Gemini, populated `.cursor/`, `.opencode/`, `.windsurf/`, `.kiro/` directories, and so on.
 
 Most users never call `apm compile` directly. `apm install` runs it as part of the integrate phase, and `apm run` auto-compiles any `.prompt.md` files referenced by a script just before execution. Reach for `apm compile` when you want to inspect what will be deployed without changing dependencies, when you are iterating on local primitives between installs, or when you only need output for one harness.
 
-The `--target` flag accepts a comma-separated list (`copilot,claude,cursor,opencode,codex,gemini,windsurf,agent-skills`) or `all`. `--dry-run` prints placement decisions without writing files. `--validate` checks primitive frontmatter and structure without producing output. `--watch` re-runs compilation on every change.
+The `--target` flag accepts a comma-separated list (`copilot,claude,cursor,opencode,codex,gemini,windsurf,kiro,agent-skills`) or `all`. `--dry-run` prints placement decisions without writing files. `--validate` checks primitive frontmatter and structure without producing output. `--watch` re-runs compilation on every change.
 
 **Common surprises**
 
diff --git a/src/apm_cli/adapters/client/kiro.py b/src/apm_cli/adapters/client/kiro.py
index e15226841..730ad28e5 100644
--- a/src/apm_cli/adapters/client/kiro.py
+++ b/src/apm_cli/adapters/client/kiro.py
@@ -16,6 +16,7 @@
 from pathlib import Path
 from typing import Any
 
+from ...utils.atomic_io import atomic_write_text
 from ...utils.console import _rich_error, _rich_success
 from .copilot import CopilotClientAdapter
 
@@ -72,8 +73,11 @@ def update_config(self, config_updates: dict[str, dict[str, Any]]) -> bool | Non
         current_config[self.mcp_servers_key].update(config_updates)
 
         config_path.parent.mkdir(parents=True, exist_ok=True)
-        with open(config_path, "w", encoding="utf-8") as f:
-            json.dump(current_config, f, indent=2)
+        atomic_write_text(
+            config_path,
+            json.dumps(current_config, indent=2),
+            new_file_mode=0o600,
+        )
         os.chmod(config_path, 0o600)
         return True
 
diff --git a/src/apm_cli/commands/install.py b/src/apm_cli/commands/install.py
index fbeca6a6a..2b2b8ee6c 100644
--- a/src/apm_cli/commands/install.py
+++ b/src/apm_cli/commands/install.py
@@ -888,7 +888,7 @@ def _handle_mcp_install(
 @click.option(
     "--runtime",
     help=(
-        "Target specific runtime only (copilot, codex, vscode, cursor, opencode, gemini, claude, windsurf, intellij)"
+        "Target specific runtime only (copilot, codex, vscode, cursor, opencode, gemini, claude, windsurf, kiro, intellij)"
     ),
 )
 @click.option("--exclude", help="Exclude specific runtime from installation")
@@ -961,7 +961,7 @@ def _handle_mcp_install(
     "global_",
     is_flag=True,
     default=False,
-    help="Install to user scope (~/.apm/) instead of the current project. MCP servers target global-capable runtimes only (Copilot CLI, Codex CLI, JetBrains Copilot).",
+    help="Install to user scope (~/.apm/) instead of the current project. MCP servers target global-capable runtimes only (Copilot CLI, Codex CLI, Gemini CLI, Kiro, JetBrains Copilot).",
 )
 @click.option(
     "--ssh",
diff --git a/src/apm_cli/integration/kiro_hook_integrator.py b/src/apm_cli/integration/kiro_hook_integrator.py
index d96a2e379..5386e52aa 100644
--- a/src/apm_cli/integration/kiro_hook_integrator.py
+++ b/src/apm_cli/integration/kiro_hook_integrator.py
@@ -20,6 +20,7 @@
     _emit_hook_event_diagnostics,
     _filter_hook_files_for_target,
 )
+from apm_cli.utils.atomic_io import atomic_write_text
 from apm_cli.utils.path_security import ensure_path_within
 from apm_cli.utils.paths import portable_relpath
 
@@ -170,7 +171,7 @@ def _write_kiro_hook_docs(
                     files_skipped += 1
                     continue
 
-                target_path.write_text(rendered, encoding="utf-8")
+                atomic_write_text(target_path, rendered, new_file_mode=0o600)
                 os.chmod(target_path, 0o600)
                 files_integrated += 1
                 target_paths.append(target_path)
diff --git a/tests/unit/core/test_target_detection.py b/tests/unit/core/test_target_detection.py
index cbb91becb..69d8894da 100644
--- a/tests/unit/core/test_target_detection.py
+++ b/tests/unit/core/test_target_detection.py
@@ -223,6 +223,10 @@ def test_gemini_target(self):
         """AGENTS.md compiled for gemini target (GEMINI.md imports it)."""
         assert should_compile_agents_md("gemini") is True
 
+    def test_kiro_target(self):
+        """AGENTS.md compiled for kiro as a cross-harness fallback."""
+        assert should_compile_agents_md("kiro") is True
+
 
 class TestShouldCompileClaudeMd:
     """Tests for should_compile_claude_md function."""
diff --git a/tests/unit/install/test_no_policy_flag.py b/tests/unit/install/test_no_policy_flag.py
index a38efebe8..1955adaae 100644
--- a/tests/unit/install/test_no_policy_flag.py
+++ b/tests/unit/install/test_no_policy_flag.py
@@ -117,6 +117,13 @@ def test_update_help_does_not_show_no_policy(self):
         assert result.exit_code == 0
         assert "--no-policy" not in result.output
 
+    def test_install_help_lists_kiro_runtime_and_global_scope(self):
+        result = self.runner.invoke(cli, ["install", "--help"])
+        assert result.exit_code == 0
+        normalized = " ".join(result.output.split())
+        assert "windsurf, kiro, intellij" in normalized
+        assert "Gemini CLI, Kiro, JetBrains Copilot" in normalized
+
     def test_help_text_is_plain_ascii(self):
         """Help text must be plain ASCII per cli.instructions.md."""
         result = self.runner.invoke(cli, ["install", "--help"])

From 2ca532e8ef548b95eec139628fe8badfeee166bd Mon Sep 17 00:00:00 2001
From: danielmeppiel <danielmeppiel@users.noreply.github.com>
Date: Thu, 11 Jun 2026 16:43:13 +0200
Subject: [PATCH 6/9] Polish Kiro target review follow-ups

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 CHANGELOG.md                                  |  2 +-
 .../docs/getting-started/first-package.md     |  8 ++++----
 .../src/content/docs/reference/cli/install.md |  2 +-
 src/apm_cli/commands/compile/cli.py           |  2 +-
 src/apm_cli/commands/install.py               |  2 +-
 tests/unit/integration/test_kiro_target.py    | 19 +++++++++++++++++++
 tests/unit/test_kiro_mcp.py                   |  2 ++
 7 files changed, 29 insertions(+), 8 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 027aef9d5..abe38f1d8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 
 - `apm install --target kiro` deploys Kiro IDE steering, skills, hooks,
-  and MCP config to the documented `.kiro/` layout. (closes #702) (#1741)
+  and MCP config to the documented `.kiro/` layout. (by @TibRib; closes #702) (#1741)
 - `apm install --target hermes` and `apm compile -t hermes` add the Hermes
   agent (Nous Research) as a new experimental target (opt in via
   `apm experimental enable hermes`). Skills deploy to `.agents/skills/<name>/SKILL.md`
diff --git a/docs/src/content/docs/getting-started/first-package.md b/docs/src/content/docs/getting-started/first-package.md
index dc79df7b5..737598408 100644
--- a/docs/src/content/docs/getting-started/first-package.md
+++ b/docs/src/content/docs/getting-started/first-package.md
@@ -17,8 +17,8 @@ Otherwise, start here.
 
 - APM installed -- see [Installation](./installation/).
 - A GitHub account and an empty repo for publishing (step 5).
-- A runtime where you can try the result: GitHub Copilot, Claude Code, or
-  Cursor.
+- A runtime where you can try the result: GitHub Copilot, Claude Code, Kiro,
+  or Cursor.
 
 ## 1. Scaffold
 
@@ -211,8 +211,8 @@ priority chain: `--target` flag > `apm.yml` `targets:` > auto-detect from
 filesystem signals (`.claude/`, `CLAUDE.md`, `.cursor/`, `.github/copilot-instructions.md`,
 `.codex/`, `.gemini/`, `GEMINI.md`, `.opencode/`, `.windsurf/`, `.kiro/`). The example layout
 above shows `.github/` because `.github/copilot-instructions.md` exists in the
-project; if you also have `.claude/`, `.cursor/`, `.opencode/`, or `.gemini/`, those
-directories get populated too. With no signal at all, `apm install` exits with
+project; if you also have `.claude/`, `.cursor/`, `.opencode/`, `.gemini/`, or
+`.kiro/`, those directories get populated too. With no signal at all, `apm install` exits with
 code 2 and a teaching message instead of silently picking a target -- declare an
 intent explicitly via `--target copilot` (or another harness), or by adding
 `targets: [copilot]` to `apm.yml`. Run `apm targets` to inspect what APM detects
diff --git a/docs/src/content/docs/reference/cli/install.md b/docs/src/content/docs/reference/cli/install.md
index 8f5e52b49..c6a190d35 100644
--- a/docs/src/content/docs/reference/cli/install.md
+++ b/docs/src/content/docs/reference/cli/install.md
@@ -50,7 +50,7 @@ With no arguments it installs everything from `apm.yml`. With one or more `PACKA
 | `--runtime VALUE` | unset | Legacy alias for `--target` (single value only). Still accepted; prefer `--target`. |
 | `--exclude VALUE` | unset | Skip a single runtime that auto-detect or `targets:` would otherwise enable. |
 | `--only apm\|mcp` | both | Install only APM packages or only MCP servers. |
-| `-g`, `--global` | off | Install to user scope (`~/.apm/`) instead of the current project. MCP servers deploy only to global-capable runtimes (Copilot CLI, Codex CLI, Gemini CLI, Kiro, JetBrains Copilot). |
+| `-g`, `--global` | off | Install to user scope (`~/.apm/`) instead of the current project. MCP servers deploy only to global-capable runtimes, such as Copilot CLI, Claude Code, Codex CLI, Gemini CLI, Kiro, Windsurf, and JetBrains Copilot. |
 | `--legacy-skill-paths` | off | Deploy skills to per-client paths (`.cursor/skills/`, `.github/skills/`, ...) instead of the converged `.agents/skills/`. Env: `APM_LEGACY_SKILL_PATHS=1`. |
 
 ### Policy and trust
diff --git a/src/apm_cli/commands/compile/cli.py b/src/apm_cli/commands/compile/cli.py
index 697ec4770..19f319256 100644
--- a/src/apm_cli/commands/compile/cli.py
+++ b/src/apm_cli/commands/compile/cli.py
@@ -791,7 +791,7 @@ def _coerce_provenance_targets(value):
     "-t",
     type=TargetParamType(),
     default=None,
-    help="Target platform (comma-separated). Values: copilot, claude, cursor, opencode, codex, gemini, windsurf, agent-skills, all. 'agent-skills' deploys to .agents/skills/ (cross-client). 'all' = copilot+claude+cursor+opencode+codex+gemini+windsurf (excludes agent-skills); combine with 'agent-skills' for both.",
+    help="Target platform (comma-separated). Values: copilot, claude, cursor, opencode, codex, gemini, windsurf, kiro, agent-skills, all. 'agent-skills' deploys to .agents/skills/ (cross-client). 'all' = copilot+claude+cursor+opencode+codex+gemini+windsurf+kiro (excludes agent-skills); combine with 'agent-skills' for both.",
 )
 @click.option(
     "--dry-run",
diff --git a/src/apm_cli/commands/install.py b/src/apm_cli/commands/install.py
index 2b2b8ee6c..b110fa1e1 100644
--- a/src/apm_cli/commands/install.py
+++ b/src/apm_cli/commands/install.py
@@ -938,7 +938,7 @@ def _handle_mcp_install(
     "target",
     type=TargetParamType(),
     default=None,
-    help="Target harness(es) to deploy to. Comma-separated for multiple: --target claude,cursor. Repeating the flag (e.g. '-t a -t b') is NOT supported -- only the last value wins; use commas. Highest-priority entry in the resolution chain (--target > apm.yml targets: > auto-detect). Values: copilot, claude, cursor, opencode, codex, gemini, windsurf, agent-skills, all. 'agent-skills' deploys to .agents/skills/ (cross-client). 'all' = copilot+claude+cursor+opencode+codex+gemini+windsurf (excludes agent-skills); combine with 'agent-skills' for both. 'copilot-cowork' is also accepted when the copilot-cowork experimental flag is enabled (run 'apm experimental enable copilot-cowork'). 'copilot-app' is also accepted when the copilot-app experimental flag is enabled (run 'apm experimental enable copilot-app'). Note: '--target all' on 'apm compile' is deprecated; use 'apm compile --all' instead.",
+    help="Target harness(es) to deploy to. Comma-separated for multiple: --target claude,cursor. Repeating the flag (e.g. '-t a -t b') is NOT supported -- only the last value wins; use commas. Highest-priority entry in the resolution chain (--target > apm.yml targets: > auto-detect). Values: copilot, claude, cursor, opencode, codex, gemini, windsurf, kiro, agent-skills, all. 'agent-skills' deploys to .agents/skills/ (cross-client). 'all' = copilot+claude+cursor+opencode+codex+gemini+windsurf+kiro (excludes agent-skills); combine with 'agent-skills' for both. 'copilot-cowork' is also accepted when the copilot-cowork experimental flag is enabled (run 'apm experimental enable copilot-cowork'). 'copilot-app' is also accepted when the copilot-app experimental flag is enabled (run 'apm experimental enable copilot-app'). Note: '--target all' on 'apm compile' is deprecated; use 'apm compile --all' instead.",
 )
 @click.option(
     "--allow-insecure",
diff --git a/tests/unit/integration/test_kiro_target.py b/tests/unit/integration/test_kiro_target.py
index 76ff8ce74..bd136ba0c 100644
--- a/tests/unit/integration/test_kiro_target.py
+++ b/tests/unit/integration/test_kiro_target.py
@@ -6,6 +6,9 @@
 from datetime import datetime
 from pathlib import Path
 
+from click.testing import CliRunner
+
+from apm_cli.cli import cli
 from apm_cli.integration.hook_integrator import HookIntegrator
 from apm_cli.integration.instruction_integrator import InstructionIntegrator
 from apm_cli.integration.skill_integrator import SkillIntegrator
@@ -45,6 +48,22 @@ def _make_package_info(
     )
 
 
+def test_kiro_is_discoverable_in_target_help() -> None:
+    runner = CliRunner()
+
+    install = runner.invoke(cli, ["install", "--help"])
+    compile_result = runner.invoke(cli, ["compile", "--help"])
+
+    expected_all_targets = "copilot+claude+cursor+opencode+codex+gemini+windsurf+kiro"
+    install_help = "".join(install.output.split())
+    compile_help = "".join(compile_result.output.split())
+
+    assert install.exit_code == 0
+    assert compile_result.exit_code == 0
+    assert expected_all_targets in install_help
+    assert expected_all_targets in compile_help
+
+
 def test_kiro_target_profile_matches_ratified_layout() -> None:
     target = KNOWN_TARGETS["kiro"]
 
diff --git a/tests/unit/test_kiro_mcp.py b/tests/unit/test_kiro_mcp.py
index c70496bea..bee005a4c 100644
--- a/tests/unit/test_kiro_mcp.py
+++ b/tests/unit/test_kiro_mcp.py
@@ -49,6 +49,7 @@ def test_update_config_creates_settings_inside_existing_kiro_dir(self, tmp_path:
         data = json.loads(mcp_json.read_text(encoding="utf-8"))
         assert data["mcpServers"]["srv"]["command"] == "node"
         assert data["mcpServers"]["srv"]["args"] == ["server.js"]
+        assert mcp_json.stat().st_mode & 0o777 == 0o600
 
     def test_user_scope_writes_without_existing_kiro_dir(self, tmp_path: Path) -> None:
         with patch("pathlib.Path.home", return_value=tmp_path):
@@ -58,6 +59,7 @@ def test_user_scope_writes_without_existing_kiro_dir(self, tmp_path: Path) -> No
         mcp_json = tmp_path / ".kiro" / "settings" / "mcp.json"
         data = json.loads(mcp_json.read_text(encoding="utf-8"))
         assert data["mcpServers"]["srv"]["command"] == "node"
+        assert mcp_json.stat().st_mode & 0o777 == 0o600
 
     def test_remote_config_uses_kiro_url_headers_and_tool_extensions(self) -> None:
         adapter = KiroClientAdapter()

From fe4d5b295dc2525d74a1f0134fdcc4b958fe0db0 Mon Sep 17 00:00:00 2001
From: danielmeppiel <danielmeppiel@users.noreply.github.com>
Date: Thu, 11 Jun 2026 16:53:10 +0200
Subject: [PATCH 7/9] Align Kiro user-scope docs and help

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 .../src/content/docs/consumer/install-mcp-servers.md | 10 ++++++----
 .../docs/integrations/ide-tool-integration.md        |  2 +-
 packages/apm-guide/.apm/skills/apm-usage/commands.md |  2 +-
 src/apm_cli/adapters/client/kiro.py                  |  1 +
 src/apm_cli/commands/install.py                      |  4 ++--
 src/apm_cli/integration/kiro_hook_integrator.py      |  1 +
 src/apm_cli/integration/mcp_integrator_install.py    |  2 +-
 tests/unit/install/test_no_policy_flag.py            |  4 ++--
 tests/unit/integration/test_kiro_target.py           | 12 ++++++++++++
 9 files changed, 27 insertions(+), 11 deletions(-)

diff --git a/docs/src/content/docs/consumer/install-mcp-servers.md b/docs/src/content/docs/consumer/install-mcp-servers.md
index 548787eae..7758aeb0c 100644
--- a/docs/src/content/docs/consumer/install-mcp-servers.md
+++ b/docs/src/content/docs/consumer/install-mcp-servers.md
@@ -121,10 +121,12 @@ fails closed with the same `[x]` voice -- consistent with how
 declare one in `apm.yml`. (#1335)
 
 `apm install -g --mcp NAME` is a deliberate carve-out: it routes the
-write to each runtime's user-scope MCP config (Copilot CLI to
-`~/.copilot/mcp-config.json`, Codex CLI to `~/.codex/config.toml`,
-Gemini CLI to `~/.gemini/settings.json`, Kiro to `~/.kiro/settings/mcp.json`) and does not consult the
-project-scope `targets:` whitelist -- user-scope writes are by
+write to each runtime's user-scope MCP config (for example, Copilot CLI to
+`~/.copilot/mcp-config.json`, Claude Code to `~/.claude.json`, Codex CLI to
+`~/.codex/config.toml`, Gemini CLI to `~/.gemini/settings.json`, Windsurf to
+`~/.codeium/windsurf/mcp_config.json`, Kiro to `~/.kiro/settings/mcp.json`,
+and JetBrains Copilot to its OS-specific user config). It does not consult
+the project-scope `targets:` whitelist -- user-scope writes are by
 definition not project-bound. Workspace-only runtimes (VS Code,
 Cursor, OpenCode) are skipped at user scope.
 
diff --git a/docs/src/content/docs/integrations/ide-tool-integration.md b/docs/src/content/docs/integrations/ide-tool-integration.md
index 295652610..9576058fc 100644
--- a/docs/src/content/docs/integrations/ide-tool-integration.md
+++ b/docs/src/content/docs/integrations/ide-tool-integration.md
@@ -60,7 +60,7 @@ Each primitive type maps to a target-specific slot:
 .apm/prompts/        ->   per target: prompt files / commands
 .apm/agents/         ->   per target: agent definitions (or skill conversion)
 .apm/skills/         ->   per target: skills directory (Claude, Codex, OpenCode, .agents)
-.apm/hooks/          ->   per target: lifecycle hooks / tool hooks (see [Targets matrix](../reference/targets-matrix/))
+.apm/hooks/          ->   per target: lifecycle hooks / tool hooks (varies by target)
 mcp: in apm.yml      ->   per target: .mcp.json / settings.json / equivalent
 ```
 
diff --git a/packages/apm-guide/.apm/skills/apm-usage/commands.md b/packages/apm-guide/.apm/skills/apm-usage/commands.md
index 650e1f6c9..4b1484846 100644
--- a/packages/apm-guide/.apm/skills/apm-usage/commands.md
+++ b/packages/apm-guide/.apm/skills/apm-usage/commands.md
@@ -10,7 +10,7 @@
 
 | Command | Purpose | Key flags |
 |---------|---------|-----------|
-| `apm install [PKGS...]` | Install APM and MCP dependencies (supports APM packages, Claude skills (SKILL.md), and plugin collections (plugin.json)) | `--update` (deprecated; prefer `apm update`) refresh refs, `--refresh` re-fetch all deps from upstream and re-resolve all ref pins, `--force` overwrite (does NOT refresh refs; use `apm update` for that), `--frozen` CI-safe install that fails fast when `apm.lock.yaml` is missing or out of sync with `apm.yml` (mutually exclusive with `--update`; structural presence check only -- use `apm audit` for SHA integrity), `--dry-run`, `--verbose`, `--only [apm\|mcp]`, `--target` (comma-separated, e.g. `--target claude,cursor`; highest-priority entry in the resolution chain `--target` > apm.yml `targets:` > auto-detect; `--target all` deprecated, see `apm compile --all`; use `kiro` for Kiro IDE; use `copilot-cowork` with `--global` after `apm experimental enable copilot-cowork`; use `hermes` after `apm experimental enable hermes` to deploy skills + `AGENTS.md` and, at `--global`, MCP servers to `~/.hermes/config.yaml`), `--dev`, `-g` global (MCP deploys only to user-scope runtimes: Copilot CLI, Codex CLI, Gemini CLI, Kiro, Hermes, JetBrains Copilot), `--trust-transitive-mcp`, `--parallel-downloads N`, `--allow-insecure`, `--allow-insecure-host HOSTNAME`, `--skill NAME` install named skill(s) from a skill collection (SKILL_BUNDLE or plugin manifest; repeatable; plugin manifests accept a leaf name or manifest path; persisted in apm.yml; `'*'` resets to all), `--legacy-skill-paths` restore per-client skill dirs, `--mcp NAME` add MCP entry (NAME goes through the same `--target` > `targets:` > auto-detect resolver as APM packages, so a project whitelisting `targets: [copilot]` will not write `.cursor/mcp.json` even if `.cursor/` exists; `apm install -g --mcp NAME` writes user-scope and bypasses the project-scope gate by design), `--transport`, `--url`, `--env KEY=VAL`, `--header KEY=VAL`, `--mcp-version`, `--registry URL` custom MCP registry, `--root DIR` redirect writes (`apm_modules/`, lockfile, `.gitignore`, integrated harness files) under DIR while `apm.yml`/`.apm/`/local deps resolve from `$PWD` (mirrors `pip install --target`; created if missing; not valid with `-g`/`--global`, which exits 2) |
+| `apm install [PKGS...]` | Install APM and MCP dependencies (supports APM packages, Claude skills (SKILL.md), and plugin collections (plugin.json)) | `--update` (deprecated; prefer `apm update`) refresh refs, `--refresh` re-fetch all deps from upstream and re-resolve all ref pins, `--force` overwrite (does NOT refresh refs; use `apm update` for that), `--frozen` CI-safe install that fails fast when `apm.lock.yaml` is missing or out of sync with `apm.yml` (mutually exclusive with `--update`; structural presence check only -- use `apm audit` for SHA integrity), `--dry-run`, `--verbose`, `--only [apm\|mcp]`, `--target` (comma-separated, e.g. `--target claude,cursor`; highest-priority entry in the resolution chain `--target` > apm.yml `targets:` > auto-detect; `--target all` deprecated, see `apm compile --all`; use `kiro` for Kiro IDE; use `copilot-cowork` with `--global` after `apm experimental enable copilot-cowork`; use `hermes` after `apm experimental enable hermes` to deploy skills + `AGENTS.md` and, at `--global`, MCP servers to `~/.hermes/config.yaml`), `--dev`, `-g` global (MCP deploys only to user-scope runtimes: Copilot CLI, Claude Code, Codex CLI, Gemini CLI, Kiro, Windsurf, JetBrains Copilot, and Hermes when enabled), `--trust-transitive-mcp`, `--parallel-downloads N`, `--allow-insecure`, `--allow-insecure-host HOSTNAME`, `--skill NAME` install named skill(s) from a skill collection (SKILL_BUNDLE or plugin manifest; repeatable; plugin manifests accept a leaf name or manifest path; persisted in apm.yml; `'*'` resets to all), `--legacy-skill-paths` restore per-client skill dirs, `--mcp NAME` add MCP entry (NAME goes through the same `--target` > `targets:` > auto-detect resolver as APM packages, so a project whitelisting `targets: [copilot]` will not write `.cursor/mcp.json` even if `.cursor/` exists; `apm install -g --mcp NAME` writes user-scope and bypasses the project-scope gate by design), `--transport`, `--url`, `--env KEY=VAL`, `--header KEY=VAL`, `--mcp-version`, `--registry URL` custom MCP registry, `--root DIR` redirect writes (`apm_modules/`, lockfile, `.gitignore`, integrated harness files) under DIR while `apm.yml`/`.apm/`/local deps resolve from `$PWD` (mirrors `pip install --target`; created if missing; not valid with `-g`/`--global`, which exits 2) |
 | `apm targets` | Show resolved deployment targets for the current project (Click group; reads filesystem signals; works with or without `apm.yml`) | `--all` also include the `agent-skills` meta-target (only meaningful with `--json`), `--json` machine-readable output. No provenance line is printed (the table is the provenance). |
 | `apm uninstall PKGS...` | Remove packages (accepts `owner/repo` or `name@marketplace`) | `--dry-run`, `-g` global |
 | `apm prune` | Remove orphaned packages | `--dry-run` |
diff --git a/src/apm_cli/adapters/client/kiro.py b/src/apm_cli/adapters/client/kiro.py
index 730ad28e5..e82814c1e 100644
--- a/src/apm_cli/adapters/client/kiro.py
+++ b/src/apm_cli/adapters/client/kiro.py
@@ -78,6 +78,7 @@ def update_config(self, config_updates: dict[str, dict[str, Any]]) -> bool | Non
             json.dumps(current_config, indent=2),
             new_file_mode=0o600,
         )
+        # Keep existing config files private after updates too.
         os.chmod(config_path, 0o600)
         return True
 
diff --git a/src/apm_cli/commands/install.py b/src/apm_cli/commands/install.py
index b110fa1e1..a8af0b3b9 100644
--- a/src/apm_cli/commands/install.py
+++ b/src/apm_cli/commands/install.py
@@ -888,7 +888,7 @@ def _handle_mcp_install(
 @click.option(
     "--runtime",
     help=(
-        "Target specific runtime only (copilot, codex, vscode, cursor, opencode, gemini, claude, windsurf, kiro, intellij)"
+        "Target specific runtime only (copilot, claude, codex, cursor, gemini, kiro, opencode, windsurf, vscode, intellij)"
     ),
 )
 @click.option("--exclude", help="Exclude specific runtime from installation")
@@ -961,7 +961,7 @@ def _handle_mcp_install(
     "global_",
     is_flag=True,
     default=False,
-    help="Install to user scope (~/.apm/) instead of the current project. MCP servers target global-capable runtimes only (Copilot CLI, Codex CLI, Gemini CLI, Kiro, JetBrains Copilot).",
+    help="Install to user scope (~/.apm/) instead of the current project. MCP servers target global-capable runtimes only (Copilot CLI, Claude Code, Codex CLI, Gemini CLI, Kiro, Windsurf, JetBrains Copilot).",
 )
 @click.option(
     "--ssh",
diff --git a/src/apm_cli/integration/kiro_hook_integrator.py b/src/apm_cli/integration/kiro_hook_integrator.py
index 5386e52aa..cb9265b55 100644
--- a/src/apm_cli/integration/kiro_hook_integrator.py
+++ b/src/apm_cli/integration/kiro_hook_integrator.py
@@ -172,6 +172,7 @@ def _write_kiro_hook_docs(
                     continue
 
                 atomic_write_text(target_path, rendered, new_file_mode=0o600)
+                # Keep existing hook files private after updates too.
                 os.chmod(target_path, 0o600)
                 files_integrated += 1
                 target_paths.append(target_path)
diff --git a/src/apm_cli/integration/mcp_integrator_install.py b/src/apm_cli/integration/mcp_integrator_install.py
index 5bdfbe6a3..ac0013790 100644
--- a/src/apm_cli/integration/mcp_integrator_install.py
+++ b/src/apm_cli/integration/mcp_integrator_install.py
@@ -469,7 +469,7 @@ def _resolve_target_runtimes(
             logger.warning(msg)
         if not target_runtimes:
             logger.warning(
-                "No runtimes support user-scope MCP installation (supported: copilot, codex, gemini)"
+                "No runtimes support user-scope MCP installation (supported: copilot, claude, codex, gemini, kiro, windsurf, intellij)"
             )
             return None
 
diff --git a/tests/unit/install/test_no_policy_flag.py b/tests/unit/install/test_no_policy_flag.py
index 1955adaae..52c12d3d6 100644
--- a/tests/unit/install/test_no_policy_flag.py
+++ b/tests/unit/install/test_no_policy_flag.py
@@ -121,8 +121,8 @@ def test_install_help_lists_kiro_runtime_and_global_scope(self):
         result = self.runner.invoke(cli, ["install", "--help"])
         assert result.exit_code == 0
         normalized = " ".join(result.output.split())
-        assert "windsurf, kiro, intellij" in normalized
-        assert "Gemini CLI, Kiro, JetBrains Copilot" in normalized
+        assert "gemini, kiro, opencode" in normalized
+        assert "Claude Code, Codex CLI, Gemini CLI, Kiro, Windsurf" in normalized
 
     def test_help_text_is_plain_ascii(self):
         """Help text must be plain ASCII per cli.instructions.md."""
diff --git a/tests/unit/integration/test_kiro_target.py b/tests/unit/integration/test_kiro_target.py
index bd136ba0c..662ae79d6 100644
--- a/tests/unit/integration/test_kiro_target.py
+++ b/tests/unit/integration/test_kiro_target.py
@@ -62,6 +62,18 @@ def test_kiro_is_discoverable_in_target_help() -> None:
     assert compile_result.exit_code == 0
     assert expected_all_targets in install_help
     assert expected_all_targets in compile_help
+    assert "ClaudeCode" in install_help
+    assert "Windsurf" in install_help
+
+
+def test_kiro_runtime_discovered_in_user_scope_without_project_dir(tmp_path: Path) -> None:
+    from apm_cli.integration.mcp_integrator_install import _discover_installed_runtimes
+
+    assert not (tmp_path / ".kiro").exists()
+
+    runtimes = _discover_installed_runtimes(tmp_path, user_scope=True)
+
+    assert "kiro" in runtimes
 
 
 def test_kiro_target_profile_matches_ratified_layout() -> None:

From deaaae0099712dd6b86bf202965e40f84a1c238a Mon Sep 17 00:00:00 2001
From: danielmeppiel <danielmeppiel@users.noreply.github.com>
Date: Thu, 11 Jun 2026 16:58:25 +0200
Subject: [PATCH 8/9] Polish Kiro runtime help wording

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 src/apm_cli/commands/install.py                   | 2 +-
 src/apm_cli/integration/mcp_integrator_install.py | 2 +-
 tests/unit/install/test_no_policy_flag.py         | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/apm_cli/commands/install.py b/src/apm_cli/commands/install.py
index a8af0b3b9..58c645a2d 100644
--- a/src/apm_cli/commands/install.py
+++ b/src/apm_cli/commands/install.py
@@ -888,7 +888,7 @@ def _handle_mcp_install(
 @click.option(
     "--runtime",
     help=(
-        "Target specific runtime only (copilot, claude, codex, cursor, gemini, kiro, opencode, windsurf, vscode, intellij)"
+        "Target specific runtime only (copilot, claude, codex, cursor, gemini, intellij, kiro, opencode, vscode, windsurf)"
     ),
 )
 @click.option("--exclude", help="Exclude specific runtime from installation")
diff --git a/src/apm_cli/integration/mcp_integrator_install.py b/src/apm_cli/integration/mcp_integrator_install.py
index ac0013790..4ddb4cfe8 100644
--- a/src/apm_cli/integration/mcp_integrator_install.py
+++ b/src/apm_cli/integration/mcp_integrator_install.py
@@ -469,7 +469,7 @@ def _resolve_target_runtimes(
             logger.warning(msg)
         if not target_runtimes:
             logger.warning(
-                "No runtimes support user-scope MCP installation (supported: copilot, claude, codex, gemini, kiro, windsurf, intellij)"
+                "No runtimes support user-scope MCP installation (supported: Copilot CLI, Claude Code, Codex CLI, Gemini CLI, Kiro, Windsurf, JetBrains Copilot)"
             )
             return None
 
diff --git a/tests/unit/install/test_no_policy_flag.py b/tests/unit/install/test_no_policy_flag.py
index 52c12d3d6..36d53739a 100644
--- a/tests/unit/install/test_no_policy_flag.py
+++ b/tests/unit/install/test_no_policy_flag.py
@@ -121,7 +121,7 @@ def test_install_help_lists_kiro_runtime_and_global_scope(self):
         result = self.runner.invoke(cli, ["install", "--help"])
         assert result.exit_code == 0
         normalized = " ".join(result.output.split())
-        assert "gemini, kiro, opencode" in normalized
+        assert "gemini, intellij, kiro" in normalized
         assert "Claude Code, Codex CLI, Gemini CLI, Kiro, Windsurf" in normalized
 
     def test_help_text_is_plain_ascii(self):

From db1c2037d2c6f119c1d2de1abe88a7337eedc3fb Mon Sep 17 00:00:00 2001
From: danielmeppiel <danielmeppiel@users.noreply.github.com>
Date: Thu, 11 Jun 2026 17:04:32 +0200
Subject: [PATCH 9/9] Cover Kiro askAgent hook conversion

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 tests/unit/integration/test_kiro_target.py | 38 ++++++++++++++++++++++
 1 file changed, 38 insertions(+)

diff --git a/tests/unit/integration/test_kiro_target.py b/tests/unit/integration/test_kiro_target.py
index 662ae79d6..b2dcc5782 100644
--- a/tests/unit/integration/test_kiro_target.py
+++ b/tests/unit/integration/test_kiro_target.py
@@ -247,6 +247,44 @@ def test_kiro_hooks_expand_each_apm_hook_to_individual_json(tmp_path: Path) -> N
     assert (tmp_path / ".kiro" / "hooks" / "hookify" / "hooks" / "prompt.py").exists()
 
 
+def test_kiro_hooks_convert_prompt_actions_to_ask_agent(tmp_path: Path) -> None:
+    (tmp_path / ".kiro").mkdir()
+    package_dir = tmp_path / "prompt-hooks"
+    hooks_dir = package_dir / "hooks"
+    hooks_dir.mkdir(parents=True)
+    hook_data = {
+        "hooks": {
+            "UserPromptSubmit": [
+                {
+                    "hooks": [
+                        {
+                            "type": "askAgent",
+                            "prompt": "Review the submitted prompt for policy drift.",
+                        }
+                    ]
+                }
+            ]
+        }
+    }
+    (hooks_dir / "hooks.json").write_text(json.dumps(hook_data), encoding="utf-8")
+
+    result = HookIntegrator().integrate_hooks_for_target(
+        KNOWN_TARGETS["kiro"],
+        _make_package_info(package_dir, "prompt-hooks"),
+        tmp_path,
+    )
+
+    target = tmp_path / ".kiro" / "hooks" / "prompt-hooks-hooks-promptsubmit-1.json"
+    assert result.files_integrated == 1
+    data = json.loads(target.read_text(encoding="utf-8"))
+    assert data["when"] == {"type": "promptSubmit"}
+    assert data["then"] == {
+        "type": "askAgent",
+        "prompt": "Review the submitted prompt for policy drift.",
+    }
+    assert target.stat().st_mode & 0o777 == 0o600
+
+
 def test_kiro_hooks_skip_when_project_has_no_kiro_dir(tmp_path: Path) -> None:
     package_dir = tmp_path / "hookify"
     hooks_dir = package_dir / "hooks"
