Skip to content

Agent Hooks

Working with an agent, the same frustrations come up again and again:

  • You find out too late that something broke. The agent edits a few files, you keep chatting, and several turns later you realise a change quietly broke the tests.
  • The agent touches things it shouldn’t. There are files it has no business editing, like secrets, generated code, and vendored libraries, and you end up watching every edit to catch it.

In both cases the thing you want is for something to happen automatically, at the right moment, without you having to remember to ask. That’s exactly what a hook is: a command (or a prompt) that fires on its own at a specific point in the agent’s lifecycle, so your workflow enforces itself.

By the end of this lesson you’ll have built a hook that catches the first frustration the moment it happens, plus a couple worth keeping, and you’ll know the shape that guards against the second. First, the definitions you need to get there.

A hook ties an action to a trigger, which is a moment in the agent’s lifecycle. You define hooks once as standalone files in .kiro/hooks/, and they apply across the whole workspace. The diagram below shows the loop. The orange parallelograms are the points where your hooks can run.

flowchart TD
  Start([Session starts]) --> H1[/"SessionStart"/]
  H1 --> Prompt["You submit a prompt"]
  Prompt --> H2[/"UserPromptSubmit (can block)"/]
  H2 --> Think["Agent reasons about the request"]
  Think --> Decide{"Use a tool?"}
  Decide -- yes --> H3[/"PreToolUse (can block)"/]
  H3 --> Tool["Tool runs"]
  Tool --> H4[/"PostToolUse"/]
  H4 --> FileOps{"Wrote to a file?"}
  FileOps -- yes --> H5[/"PostFileCreate / PostFileSave / PostFileDelete"/]
  H5 --> Think
  FileOps -- no --> Think
  Decide -- no --> Reply["Agent replies"]
  Reply --> More{"Another prompt?"}
  More -- yes --> Prompt
  More -- no --> Ends([Session ends])
  Ends --> H6[/"Stop"/]
  classDef hook stroke:#f59e0b,stroke-width:2px;
  class H1,H2,H3,H4,H5,H6 hook;

There are eleven triggers in all, each with its own matcher rules and blocking behavior. You don’t need to memorise them to follow this lesson. The diagram above covers the ones we’ll use. When you want the full table, matcher semantics, and exit-code behavior in one place, see the hook trigger reference appendix.

  • command runs a shell command. It receives the hook context as JSON on stdin, and its exit code carries meaning: 0 is success, and for blocking triggers (PreToolUse, UserPromptSubmit) exit code 2 blocks the action and returns the command’s STDERR to the agent.
  • agent appends a prompt string to the model’s context. No subprocess is spawned. Use it for lightweight steering and guardrails.

Each hook file is a standalone .kiro/hooks/<name>.json with a versioned schema. One file can declare several hooks:

.kiro/hooks/example.json
{
"version": "v1",
"hooks": [
{
"name": "format-on-save",
"description": "Format C# after it's saved.",
"trigger": "PostFileSave",
"matcher": "\\.cs$",
"action": { "type": "command", "command": "dotnet format" },
"timeout": 30,
"enabled": true
}
]
}
FieldRequiredDefaultDescription
nameYesnoneIdentifier for the hook
descriptionNononeDocumentation only
triggerYesnoneWhen the hook fires (see above)
matcherNoalways matchRegex filtering by tool name or file path
actionYesnone{ type: "command", command } or { type: "agent", prompt }
timeoutNo60Seconds; 0 disables. Ignored for agent actions
enabledNotrueSet false to disable without deleting

Now we’ll put hooks to work, right in the budget tracker you’ve been working in.

The budget tracker already ships with a test suite. From the project root, confirm it’s green:

Terminal window
dotnet test

You should see every test pass. That’s the safety net the first hook will watch.

Warm-up: feed the agent your project’s state

Section titled “Warm-up: feed the agent your project’s state”

Before fixing either problem, let’s build the smallest useful hook, one you’ll actually want to keep, and use it to see a hook fire for the first time.

Here’s a small pain you’ve probably already felt: when a session starts, the agent has no idea what branch you’re on or whether your working tree is dirty. You end up telling it the same context every time. A SessionStart hook can hand it that state automatically, the moment the session begins.

  1. Register a SessionStart hook that prints your git branch and a count of changed files. Pick the tab for your shell:

    .kiro/hooks/project-state.json
    {
    "version": "v1",
    "hooks": [
    {
    "name": "inject-git-state",
    "description": "Tell the agent the branch and dirty-file count at session start.",
    "trigger": "SessionStart",
    "action": {
    "type": "command",
    "command": "echo \"On branch $(git branch --show-current); $(git status --porcelain | wc -l | tr -d ' ') changed file(s).\""
    }
    }
    ]
    }
  2. Restart Kiro so it loads the hook file. The restart fires SessionStart, the hook runs, and its STDOUT is added to the agent’s context before you type anything.

  3. Ask the agent what it already knows:

    What branch am I on, and do I have uncommitted changes?

    It answers from the line your hook injected, without running a single tool. You’ve just watched a hook fire and built something worth keeping: every future session now starts with the agent already oriented.

A second shape of SessionStart: inject fixed instructions

Section titled “A second shape of SessionStart: inject fixed instructions”

The warm-up hook ran a command to compute something fresh each session (your branch can change). But sometimes the thing you want to inject never changes. It’s a standing instruction you’d otherwise paste at the start of every session. Spawning a subprocess just to echo a fixed string is wasteful, and that’s exactly what the agent action is for: it appends a prompt string straight to the model’s context, no shell involved.

Here’s a useful one. By default the agent just does the work. If you’re using the workshop to learn, you want it to explain its choices as it goes. This hook turns on a teaching mode for every session.

  1. Register a SessionStart hook with an agent action instead of a command:

    .kiro/hooks/explain-mode.json
    {
    "version": "v1",
    "hooks": [
    {
    "name": "explanatory-mode",
    "description": "Ask the agent to teach as it works, every session.",
    "trigger": "SessionStart",
    "action": {
    "type": "agent",
    "prompt": "For this session, provide brief educational insights as you work. Before and after writing code, add a short note explaining the implementation choices you made, formatted as:\n\n\u2605 Insight\n[2-3 key points specific to this codebase or the code you just wrote]\n\nKeep insights focused and relevant rather than general programming advice, and put them in the conversation, not in the code. Don't wait until the end. Explain as you go."
    }
    }
    ]
    }

    There’s no matcher and no timeout here: SessionStart doesn’t evaluate a matcher, and timeout is ignored for agent actions because nothing is spawned.

  2. Restart Kiro so it loads the hook, then give it any coding task:

    Add a `/health` endpoint to `Program.cs` that returns the text "ok".

    The agent does the work and explains its reasoning as it goes. The ★ Insight notes appear in the conversation without you ever asking for them. That instruction was injected by the hook the moment the session began.

Problem 1: catch regressions the moment they happen

Section titled “Problem 1: catch regressions the moment they happen”

The first frustration was finding out too late that something broke. PostFileSave fires after a file is written, so we’ll match C# files and run dotnet test on every edit the agent makes.

  1. Create the hook file:

    .kiro/hooks/tests.json
    {
    "version": "v1",
    "hooks": [
    {
    "name": "tests-on-save",
    "description": "Run the test suite after any C# file is saved.",
    "trigger": "PostFileSave",
    "matcher": "\\.cs$",
    "action": { "type": "command", "command": "dotnet test" }
    }
    ]
    }

    The hooks live alongside the app:

    • Program.cs
    • Money.cs
    • Directory.kiro/
      • Directoryhooks/
        • tests.json
  2. Restart Kiro so it loads the new hook file, then ask the agent for a normal edit:

    Add a short doc comment above the Money.Format method.

    As the agent saves the file, the hook runs dotnet test. The tests pass, so the hook stays quiet.

  3. Now introduce a regression:

    In Money.cs, change Format so it drops the cents and returns whole dollars.

    When the agent saves the file, dotnet test exits non-zero (the Money.Format tests fail) and the failure is surfaced to you. The hook caught the regression the moment it happened, instead of three turns later.