This website uses cookies

Read our Privacy policy and Terms of use for more information.

What we are building

The pitch for autonomous coding agents is that you hand over a multi-step task, walk away, and come back to working code. The reality is that an agent with unrestricted shell access and no undo button is a liability, not a productivity tool. One bad rm, one rogue git push, one refactor that quietly breaks half your imports, and you spend the afternoon cleaning up instead of shipping.

This tutorial fixes that. We will take Kiro CLI, the terminal agent from the same team behind the Kiro IDE we used in episode 16, and wire it so it can run a long task without supervision but cannot do anything you did not allow. The build has three moving parts: a custom agent whose tool permissions are an allowlist, not a free-for-all; workspace checkpoints that snapshot the repo before the agent touches it; and a goal loop that keeps the agent iterating until a concrete acceptance criterion passes.

The finished thing is a single command, /goal, that delegates a real refactor to the agent on a real repo, with a verifiable definition of done ("pytest passes and mypy reports no errors"), and a /checkpoint restore that puts everything back if you do not like the result. The non-obvious design choice is that the safety does not come from watching the agent. It comes from making the blast radius small before you start.

Prerequisites

You need Kiro CLI installed and authenticated. Kiro CLI is free to use with a Builder ID, so you do not need an AWS account with billing set up for this tutorial. If you completed episode 16 you already have a Kiro login.

You also need Python 3.10 or newer, plus pytest and mypy on your PATH, because the demo repo is a small Python package and the agent's acceptance criterion is "the test suite passes and the type checker is clean." Install them with pip install pytest mypy if you do not have them. Git is required because Kiro's checkpoint feature is built on a shadow git repository and auto-enables inside git repos.

Assumed knowledge: you are comfortable reading Python, you have used a terminal AI agent at least once, and you understand the difference between a tool an agent can call and a permission to call it without asking. That last distinction is the whole tutorial.

Setup

Install Kiro CLI per the official installation page if you have not already, then confirm the binary and your login:

kiro-cli --version
kiro-cli doctor
kiro-cli whoami

doctor diagnoses common install and config problems and whoami prints your auth method (Builder ID, IAM Identity Center, or social login). If whoami says you are not logged in, run kiro-cli login and complete the browser flow.

Now create a small but real repository for the agent to work on. We want something with functions that lack type hints and a test that already passes, so the agent has a concrete, checkable job:

mkdir kiro-autorun && cd kiro-autorun
git init -q
mkdir -p src tests .kiro/agents
cat > src/inventory.py <<'PY'
def add_item(items, name, qty):
    items[name] = items.get(name, 0) + qty
    return items

def remove_item(items, name, qty):
    if items.get(name, 0) < qty:
        raise ValueError("not enough stock")
    items[name] -= qty
    return items

def total_units(items):
    return sum(items.values())
PY
cat > tests/test_inventory.py <<'PY'
from src.inventory import add_item, remove_item, total_units

def test_flow():
    inv = {}
    add_item(inv, "bolt", 10)
    remove_item(inv, "bolt", 3)
    assert total_units(inv) == 7
PY
python -m pytest -q

The smoke test is the last line: pytest should report one passing test. That green test is the baseline. When the agent is done, the test must still pass, which is how we know the refactor did not break behavior.

Step 1: Define a guardrailed agent

The default Kiro agent asks permission before every write or shell command, which defeats autonomy. We want the opposite: pre-approve a narrow set of safe actions, and forbid the dangerous ones outright. That is what a custom agent config is for. Create .kiro/agents/refactor-bot.json:

{
  "name": "refactor-bot",
  "description": "Autonomous refactor agent, fenced to src and tests",
  "prompt": "You refactor Python without changing behavior. Keep all existing tests passing. Make no network calls and touch no files outside src/ and tests/.",
  "tools": ["read", "write", "shell"],
  "allowedTools": ["read", "write", "shell"],
  "toolsSettings": {
    "write": { "allowedPaths": ["src/**", "tests/**"] },
    "shell": {
      "allowedCommands": ["python .*", "python -m pytest.*", "mypy .*"],
      "deniedCommands": ["git push.*", "git commit.*", "rm .*", "curl.*", "pip install.*"],
      "autoAllowReadonly": true
    }
  },
  "model": "claude-sonnet-4"
}

Read this config as a contract. The tools field says the agent can use read, write, and shell at all. The allowedTools field says it may use those three without stopping to ask you, which is what makes the run autonomous. The toolsSettings block is where the fence goes up. write.allowedPaths restricts every file write to src/ and tests/, so the agent physically cannot rewrite your shell profile, your git config, or anything under ~/.kiro. The shell.allowedCommands list is an allowlist of command patterns the agent may run unattended, and deniedCommands is a hard block that wins even if a command would otherwise match. Pushing, committing, deleting files, network fetches, and installing packages are all off the table.

autoAllowReadonly lets harmless read-only shell commands through without prompting. Validate the file before you rely on it:

kiro-cli agent validate .kiro/agents/refactor-bot.json

A note that the docs are blunt about: enabling write and shell tools gives the agent the same file-system permissions as your user account, minus whatever you fenced off. The allowedPaths and deniedCommands settings are doing real work here. Do not skip them and then run unattended.

Step 2: Turn on checkpoints and snapshot the repo

Checkpoints are Kiro's undo button for autonomous work. The feature creates a shadow bare git repository to track file changes, takes a checkpoint per conversation turn, and adds sub-checkpoints for each tool use. Restoring a checkpoint reverts both the files and the conversation history to that point. It is marked experimental, so you enable it explicitly:

kiro-cli settings chat.enableCheckpoint true

Now launch the session with your agent and create the baseline snapshot before the agent has done anything:

kiro-cli chat --agent refactor-bot

Inside the session:

> /checkpoint init
> /checkpoint list

init snapshots the current workspace state and list shows your checkpoints with timestamps and file stats. You should see a single checkpoint representing the clean repo. This is the state you can always get back to. Because checkpoints also capture conversation history, restoring later does not just revert files, it rewinds the agent's memory of what it did, which keeps the agent from getting confused about a state that no longer exists.

If you are working in a directory that is not a git repo, checkpoints still work in an ephemeral mode, but inside a real git repo they auto-enable and behave more predictably. We ran git init in setup precisely so this step is solid.

Step 3: Delegate the work with a goal loop

A normal prompt runs once. A goal loop runs the agent through repeated cycles of plan, implement, and verify until a concrete acceptance criterion passes or you cancel. This is the autonomy primitive. The criterion matters more than the instruction, so spell out what "done" means in checkable terms. Still inside the chat session:

> /goal --max 8 add type hints to every function in src/inventory.py, then run python -m pytest -q and mypy src, and do not stop until pytest passes and mypy reports no errors

The --max 8 caps the loop at eight iterations so a goal that cannot converge does not run forever; the default is five. The agent will plan the edits, write type hints into src/inventory.py, run the test suite and the type checker, read the output, and if mypy complains it will edit again and re-run. Because we gave it python -m pytest and mypy src as the verification commands and both are on the agent's allowedCommands allowlist, it can run them unattended. Because write.allowedPaths is src/** and tests/**, every edit lands where you expect.

This is the moment the guardrails earn their place. The agent is now running on its own, possibly for several minutes across multiple turns, and you are not approving each step. The only reason that is safe is that you decided in Step 1 exactly what "on its own" is allowed to mean.

Step 4: Steer mid-run without cancelling

Autonomy does not mean you are locked out. If you watch the agent head down a path you do not want, for example adding a heavyweight typing dependency instead of using the standard dict and int hints, you can redirect it without killing the loop. Press Ctrl+S to open queue steering and send a correction that the agent picks up at the next tool boundary:

use only built-in types from typing, no third-party packages

The goal loop keeps running with your nudge folded in. This is the difference between supervising and micromanaging. You set the destination with /goal, let the agent drive, and tap the wheel only when it drifts. If you instead want to stop entirely, /goal clear cancels the loop and returns you to standard interactive mode. Any file changes already made stay on disk, which is exactly why the checkpoint from Step 2 is your safety net rather than the cancel command.

Step 5: Inspect, and roll back if needed

When the goal completes, do not just trust the agent's word. Look at what changed against your baseline checkpoint:

> /checkpoint list
> /checkpoint diff 1 2

diff shows the differences between two checkpoints, so you can read the exact edits the agent made to src/inventory.py. If the refactor looks good, you are done; commit it yourself, since the agent was explicitly denied git commit and git push. If the agent made a mess, restoring is one command:

> /checkpoint restore 1

That reverts the tracked changes and brings the conversation back to the clean baseline. By default, restore keeps files created after the checkpoint; if you want an exact match that also removes anything new, use /checkpoint restore 1 --hard. There is also a softer tool for exploration: /rewind forks the conversation at an earlier turn into a new session so you can try a different approach from the same starting point, while leaving the original session intact. Restore is for "undo the files," rewind is for "explore a different branch of the conversation."

Verify it works

You have a successful run when three things are true. First, the agent reported the goal complete on its own rather than hitting the iteration cap. Second, the test suite still passes and the type checker is clean. Drop to a shell and confirm independently, outside the agent:

python -m pytest -q
mypy src

Expected output: pytest reports 1 passed, and mypy prints Success: no issues found in 1 source file. Open src/inventory.py and you should see signatures like def total_units(items: dict[str, int]) -> int: that were not there before.

Third, your safety machinery is intact. Run /checkpoint list and you should see at least two checkpoints: the baseline from Step 2 and one or more from the agent's turns. If /checkpoint diff 1 2 shows edits confined to src/ and tests/ and nothing else changed, the fence held. That combination, a verified-passing build plus changes that stayed inside the box you drew, is the contract of this tutorial. If you see it, the autonomous run worked and stayed safe.

When it breaks

If the agent stops at the iteration cap without finishing, your acceptance criterion was probably too vague or too large for eight turns. Tighten the goal ("mypy reports no errors" beats "improve the types") or raise --max. A goal that loops without converging is almost always a goal whose definition of done the agent cannot actually check.

If the agent reports it cannot write a file, your allowedPaths is too tight for what you asked. The agent asked to touch something outside src/** and tests/** and the write tool refused. Either widen the path in the agent config or narrow the task. This is the guardrail working, not a bug.

If a shell command the agent needs hangs on a permission prompt or gets blocked, check it against allowedCommands and deniedCommands. Remember that deniedCommands wins over allowedCommands, so a too-broad deny pattern can block something you meant to allow. Run /tools inside the session to see the current permission state for every tool.

If /checkpoint says it is unavailable, you did not enable it. Run kiro-cli settings chat.enableCheckpoint true and restart the session, since it is an experimental feature that is off by default. And if the agent falls back to a different model with a warning, the model string in your config ("claude-sonnet-4") was not in the list your account exposes; run /model to see what is available and update the config.

Where to take it next

First, add a preToolUse hook to the agent config that appends every shell command to an audit log, so you have a record of exactly what ran during an unattended session. The hooks field supports a matcher on execute_bash and a command that reads the tool input from stdin; it is a few lines of JSON and gives you a paper trail.

Second, move the run into CI. Kiro CLI has a headless mode (kiro-cli chat --no-interactive) designed for non-interactive pipelines, so the same guardrailed agent that refactors locally can run as a gated job on a branch, with the test suite as the merge condition.

Third, scale the task from a toy module to a real chore you have been avoiding: migrating a test suite from one framework to another, or renaming a model across a codebase with no broken imports. Keep the pattern identical, snapshot first, fence the tools, give the goal a checkable acceptance criterion, and let it run. The hard part of autonomous engineering was never getting the agent to act. It was being able to trust the result. Did you make the blast radius small before you let go of the wheel?

Sources

Keep Reading