Skip to main content

Govern Kiro (AWS)

Surfaces used: coding assistant hooks Modes supported: Local Hosted Hybrid Tiers: Free (individual) Teams (org-wide enrollment) SDK: Python (pip install controlzero)

Kiro is AWS's agentic IDE, paired with kiro-cli. Control Zero deterministically evaluates Kiro CLI's preToolUse events before shell commands, file operations, MCP tools, and AWS API calls execute. A deny rule stops the call, and every evaluation is recorded in the audit trail.

Maturity
  • Kiro CLI (kiro-cli chat): GA (direct tool calls gated, and sub-agent spawns from SDK 1.13.12; a sub-agent's own tool calls only when it runs an agent config carrying our hooks). A deny rule stops the call before it runs, across every tool family, including argument-level deny rules. Validate any install with controlzero kiro verify.

    Two things are true at once here, and they are different kinds of fact. Reach is a vendor fact and is stated as scope on the tag. Maturity is a claim about our adapter, and it is the only thing the tier describes.

    What is gated, driven against kiro-cli 2.18.1 with a nonce side-effect proving each call actually ran:

    • A direct tool call from the parent reaches preToolUse with full arguments, and exit 2 stops it before it executes.
    • A sub-agent's own tool calls reach preToolUse and a child hook exiting 2 blocks them -- when that sub-agent runs an agent config that carries our hooks.

    Denying the use_subagent spawn itself also works when driven (exit 2 blocks the spawn, and the sub-agent is never created), but controlzero install kiro only wires use_subagent as a preToolUse matcher from Python SDK 1.13.12 or later. On any earlier SDK the hook is never invoked for that tool, so a deny rule naming it silently never fires -- it is a no-op that looks exactly like a rule that never matched. Check controlzero --version before relying on it. It is documented here rather than presented as available: a mitigation you cannot switch on is not coverage.

    What is not gated, and why: Kiro resolves hooks from the sub-agent's own agent config and does not inherit them from the parent. A sub-agent spawned without an explicit agent_name therefore runs against an implicit config that carries no hooks, and nothing fires inside that sub-agent. The spawn itself is unaffected: it is a tool call by the parent, the parent is hooked, and preToolUse fires for it either way. Setting chat.defaultAgent to a hooked agent does not close this: driven, the implicit agent does not resolve to chat.defaultAgent. Naming an agent_name whose config has no hooks is ungated for the same reason. That is the host's limit (kirodotdev/Kiro#7755, open, no maintainer response), not an unbuilt feature on our side, and it is why the adapter still declares its coverage partial rather than fully enforced.

    use_subagent is the only spawn tool this build exposes. delegate is experimental and disabled, agent_crew appears in the binary's alias table but is not exposed, and spawn_session is a command value inside a session tool rather than a tool name -- so do not treat them as four gateable spawn surfaces.

    Kiro matches hook matchers by alias family, not literal tool name: an execute_bash matcher fires on tool_name: "shell", and fs_write on "write".

    Do not cite Kiro #7671

    Kiro #7671 -- "hooks do not trigger in sub-agents", filed against kiro-cli 0.11.130 -- is closed as completed. The vendor fixed it. Any statement that Kiro never runs preToolUse in a sub-agent is false on current builds; the live limitation is the config-scoping one above, tracked as #7755.

Kiro IDE is the opposite case and we state it plainly: current Kiro GA ships a deterministic hook engine that delivers tool identity and arguments and blocks on exit code 2, and Control Zero is not yet wired to it end to end. That gap is ours, not the vendor's.

Use the Python SDK for Kiro. That is the install path below, and it is the path that carries prompt-surface scanning and correct kiro_cli audit attribution. The published Node SDK wires Kiro CLI tool calls through the deprecated controlzero kiro init, but it does not scan the prompt surface and its audit rows are attributed to the Claude Code adapter, so Kiro coverage there is narrower than it looks. controlzero install kiro is Python-only syntax.

requires_approval (HITL) policies are enforced on the CLI as a hard block (stop-and-review), because Kiro CLI has no native interactive approval state. :::

Install

Install the Control Zero CLI (requires Python 3.9+):

pip install controlzero

Wire Kiro CLI the same way as every other agent:

controlzero install kiro --surface cli

The installer writes a default policy to ~/.controlzero/policy.yaml (ALLOW ALL by default, so Kiro keeps working out of the box) and registers the enforcing hook. Add a deny rule and the matching Kiro CLI call stops before execution. Remove the wiring with controlzero uninstall kiro.

note

The older controlzero kiro init (and controlzero kiro init --uninstall) still works as a deprecated alias, so existing scripts keep running -- but prefer controlzero install kiro / controlzero uninstall kiro.

How it works

Kiro CLI passes each event as snake_case JSON on stdin, byte-compatible with what controlzero hook-check consumes. The installer pins the hook to "<python>" -m controlzero hook-check --strict (the <python> path is machine-specific), so it cannot silently resolve to a different binary on PATH. Strict mode fails closed when it cannot establish coverage. Signed policy bundles are verified before evaluation, with tampering detected rather than silently accepted.

Every Kiro CLI hook evaluation writes an allow or deny result to ~/.controlzero/audit.log with the Kiro CLI audit source. Because coverage is tracked per event, “the hook did not run” remains distinguishable from “the hook ran and found no violation.”

Policy

Policies live at ~/.controlzero/policy.yaml. Rules are evaluated top to bottom, first match wins. A per-project controlzero.yaml overrides the global file.

version: '1'

rules:
# Block destructive shell on the CLI surface.
# Author shell rules against the canonical `bash:*` tool: Control Zero
# normalizes every agent's shell tool (Kiro's `execute_bash`, Codex's `shell`,
# Gemini's `run_shell_command`, ...) to `Bash`, so one canonical shell rule
# can be reused across enforcing hosts, including Kiro CLI.
- id: deny-shell
deny: 'bash:*'
reason: 'Kiro runs in read-only mode in this workspace'

# Block AWS mutations from the agent
- id: deny-aws-writes
deny: 'use_aws'
reason: 'AWS mutations go through CI, not the agent'

# Allow everything else (remove for deny-by-default)
- id: allow-everything-else
allow: '*'
reason: 'Default allow. Tighten by adding deny rules above this one.'

Common Kiro tool names

Tool (CLI aliases)What it does
fs_read / readFile read
fs_write / writeFile write
execute_bash / shellShell execution
use_aws / awsAWS API call
@<server>/<tool>MCP tools, namespaced

Test a decision without running Kiro

echo '{"hook_event_name":"preToolUse","tool_name":"execute_bash","tool_input":{"command":"rm -rf /"}}' \
| controlzero hook-check

Kiro IDE

Kiro IDE does not enforce or audit through the currently installed hooks: Kiro 1.0.x loads *.json, while the installer writes *.kiro.hook. Use Kiro CLI or the Python SDK directly for enforcement.

Verify

# Confirm the CLI hook is wired AND blocks a synthetic deny (exit 0 = enforcing):
controlzero kiro verify

# CLI: reload your shell, then run kiro-cli chat -- every tool call shows
# "[Control Zero] Allowed: <tool>" or a branded deny reason.
kiro-cli chat

# Tail the audit log
tail -f ~/.controlzero/audit.log

See also