# Setting up a coding agent for a Python project

> Source: https://learn-python.com/ai/agent-setup/
> Part of Learn Python, free to read.

Most people try a coding agent on a Python repo, get plausible-looking code that fails in a way they only discover twenty minutes later, and conclude the tool is overhyped. Usually the tool was fine. The setup was missing.

An agent is a loop: propose an edit, run something, read the result, adjust. If there is nothing meaningful for it to run, the loop collapses into a single guess with no correction step, and you are back to copy-pasting from a chat window. **Everything below exists to give the loop something real to push against.**

:::note
This page is deliberately tool-agnostic. It applies to Claude Code, Cursor, Codex CLI, Aider, Cline, Windsurf and anything else that can edit files and run commands. Where behaviour genuinely differs, we say which tool.
:::

## 1. Make the environment reproducible in one command

The single biggest cause of wasted agent turns is an environment the agent cannot recreate. If your project needs a specific Python version, a virtualenv someone activated by hand three weeks ago, and two environment variables that live in a colleague's shell, the agent will spend its turns debugging your machine instead of your code.

Use `uv`. It resolves, installs and pins in one place, and it is fast enough that an agent can afford to run it repeatedly.

```bash
# one-time, in the repo root
uv init --python 3.12
uv add --dev pytest pytest-cov ruff mypy
uv sync
```

Now the entire environment is two files — `pyproject.toml` and `uv.lock` — and any agent, on any machine, gets an identical one with `uv sync`.

:::tip Why this matters more than it looks
An agent that can run `uv run pytest` and get a real answer in four seconds will iterate five times before showing you anything. An agent that has to ask you to activate a venv will show you its first guess. The difference in output quality is not subtle.
:::

## 2. Give it exactly four commands

Agents do better with a small, named set of verbs than with a README paragraph describing your workflow. Put them somewhere executable — a `Makefile`, a `justfile`, or `[tool.uv]` scripts — and name them predictably.

```makefile Makefile
.PHONY: install test lint typecheck check

install:
	uv sync

test:
	uv run pytest -q

lint:
	uv run ruff check --fix . && uv run ruff format .

typecheck:
	uv run mypy src

check: lint typecheck test
```

`make check` is now the whole contract. The agent does not have to guess whether you use `black` or `ruff format`, whether tests live in `tests/` or beside the source, or whether type checking is expected to pass. One command, one exit code.

:::warn Make `check` actually fail
A `make check` that exits 0 on a broken codebase is worse than no command at all — it teaches the agent that finished means finished. If your type checking currently produces 400 errors, do not wire it into `check` yet. Baseline it first (see [The verification loop](/ai/feedback-loops/)), then turn it on.
:::

## 3. Decide what it may run without asking

Every serious agent has a permission model. Configure it once; otherwise you spend your session clicking Allow, which trains you to click Allow without reading — the single most dangerous habit in agentic development.

An allowlist is the coarse control. For policy that reacts to *what* is being run — deny this shape, ask before that one — you want a hook; there is a full reference in [harness hooks](https://codelearningdojo.com/harness-hooks/). And for the containment that catches what no policy anticipated — a bad turn, an injected instruction — see [sandboxing a coding agent](https://codelearningdojo.com/sandboxing/).

The rule of thumb: **auto-allow anything that only reads or only affects the working tree; always confirm anything that touches the network, the package index, or another machine.**

```json .claude/settings.json
{
  "permissions": {
    "allow": [
      "Bash(uv run pytest:*)",
      "Bash(uv run ruff:*)",
      "Bash(uv run mypy:*)",
      "Bash(make test)",
      "Bash(make lint)",
      "Bash(make check)",
      "Bash(git status)",
      "Bash(git diff:*)",
      "Bash(git log:*)"
    ],
    "ask": [
      "Bash(uv add:*)",
      "Bash(pip install:*)",
      "Bash(git push:*)",
      "Bash(gh pr create:*)"
    ],
    "deny": [
      "Bash(curl:*)",
      "Bash(rm -rf:*)",
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(./secrets/**)"
    ]
  }
}
```

Cursor and Windsurf have equivalent allowlists in their settings UI; Aider has `--yes-always` (don't) and per-command confirmation (do).

:::danger The .env rule is not optional
Agents read files to build context, and context goes to a model provider. Deny-listing `.env`, `.env.*`, `secrets/`, `*.pem` and `~/.aws` costs you nothing and removes an entire category of incident. Do it before your first session, not after. It is also one edge of the [prompt-injection triangle](https://codelearningdojo.com/prompt-injection/) — data the agent cannot read cannot be exfiltrated.
:::

## 4. Work in a sandbox you are willing to lose

The best posture for agentic work is one where a bad turn costs you nothing. In practice:

- **A branch, always.** `git switch -c agent/thing`. Never point an agent at a dirty `main`.
- **Commit before you start.** An uncommitted working tree is the one thing an agent can genuinely destroy.
- **Consider a worktree** for longer runs, so you can keep working in the main checkout: `git worktree add ../proj-agent -b agent/thing`.
- **Containers for anything untrusted.** If you are letting an agent run code it wrote against a real database, put it in a container with a throwaway copy.

## 5. Point it at the tests, not at the code

The most common bad instruction is *"add feature X to `service.py`"*. The most common good one is *"here's a failing test that describes feature X; make it pass without changing the test."*

The second version gives the loop a termination condition that the model cannot talk itself out of. It also forces you to say what you actually want precisely enough to assert on — which is where half the value comes from, agent or not.

```python tests/test_pricing.py
def test_bulk_discount_applies_at_ten_units():
    cart = Cart([Line(sku="A1", qty=10, unit_price=Decimal("5.00"))])
    assert cart.total() == Decimal("45.00")   # 10% off at 10+


def test_bulk_discount_does_not_apply_at_nine():
    cart = Cart([Line(sku="A1", qty=9, unit_price=Decimal("5.00"))])
    assert cart.total() == Decimal("45.00")   # 9 x 5.00, no discount
```

Note the second test. A single test is an invitation to hardcode; the pair pins the boundary. Writing the boundary case yourself is worth more than any prompt engineering you will ever do.

:::promo jetbrains
:::

## 6. Write the AGENTS.md last, not first

Everyone's instinct is to start by writing a long instructions file. Don't. You do not yet know which mistakes your agent makes on *your* codebase, so you will write generic advice the model already follows, and you will bloat the context for nothing.

Run three or four real tasks first. Every time you find yourself correcting the same thing twice, that correction is a line in `AGENTS.md`. Everything else is noise.

We wrote a whole page on getting this file right: [Writing an AGENTS.md for Python that agents actually follow](/ai/agents-md/).

## What this costs

Agent sessions are billed, and the bill is driven by context size more than by how much you ask for. The levers — prompt caching, pruning unused MCP servers, starting a fresh session when the task changes — are in [what tokens actually cost](https://codelearningdojo.com/token-economics/).

## The twenty-minute checklist

```text
[ ] uv-managed environment, uv.lock committed
[ ] make install / test / lint / typecheck / check all work from a clean clone
[ ] make check genuinely fails on a broken tree
[ ] permission allowlist configured; .env and secrets/ denied
[ ] working on a branch, tree committed before each run
[ ] at least one test file the agent can run in under 10 seconds
[ ] AGENTS.md deliberately left empty until you have evidence for it
```

If all seven are true, you have a working loop. Everything after this is refinement.

## Common questions

### Do I need a different setup for Claude Code, Cursor and Codex?

No. The substance — reproducible environment, a small set of named commands, a permission allowlist, a fast test — is identical across tools. Only the configuration file names differ: `.claude/settings.json` and `CLAUDE.md` for Claude Code, `.cursor/rules` for Cursor, `AGENTS.md` for Codex and increasingly for everything else. Write `AGENTS.md` and symlink the tool-specific names to it.

### Should the agent be allowed to install packages?

Put package installation behind a confirmation, not on the allowlist. Language models hallucinate package names at a measurable rate, and attackers register the hallucinated ones — see [Hallucinated packages and slopsquatting](/review/dependencies/). A one-second glance at `uv add <name>` before it runs is the cheapest security control available to you.

### Is it worth doing all this for a small script?

No. For a fifty-line script, ask in a chat window and read the output. This setup pays off on a codebase you will still be maintaining in six months, where the cost of a wrong edit is measured in debugging time rather than in re-reading a screen.

### What about test coverage — should I make the agent chase a number?

Avoid it. Coverage targets are trivially gamed by tests that execute code without asserting anything useful, and an agent optimising for a number will produce exactly that. Ask for tests that pin behaviour at boundaries, then read them yourself.
