Agentic Coding

Agentic Coding refers to LLM-driven workflows that autonomously generate, evaluate, and iterate on code by invoking tools, running tests, and feeding results back into the model. It matters when you need to automate repetitive implementation across stacks, prototype functional code quickly, or manage multi-step coding tasks with bounded autonomy. Unlike one-shot prompting, agentic systems maintain internal state, plan sequences of actions, and handle failure recovery through explicit loops.

Initiating an Agentic Loop

The most common pattern begins with a prompt that asks the model to produce runnable code, then validates the output before deciding whether to stop or request a revision. A concrete, real‑world implementation can be expressed as a bash function that uses only shipping tools:

agentic_loop() {
  local prompt="$1"
  local max_turns="${2:-5}"
  local turn=0
  local code=""

  while [ "$turn" -lt "$max_turns" ]; do
    turn=$((turn + 1))
    # 1. Ask the LLM for code via a standard HTTP API
    local response
    response=$(curl -s -X POST https://api.example.com/v1/chat/completions \
      -H "Authorization: Bearer $API_KEY" \
      -H "Content-Type: application/json" \
      -d @- <<EOF
{
  "model": "claude-3-haiku-20240307",
  "messages": [{"role": "user", "content": "$prompt"}],
  "temperature": 0.2,
  "max_tokens": 1024
}
EOF
)

    # 2. Extract the generated text using a real, shipping parser
    code=$(echo "$response" | jq -r '.choices[0].message.content // empty')

    [ -z "$code" ] && { echo "No code returned on turn $turn"; break; }

    # 3. Write the candidate to a temporary file
    local candidate="/tmp/agent_candidate_$$.py"
    printf '%s\n' "$code" > "$candidate"

    # 4. Run the real test suite; pytest is a standard, shipping tool
    local test_output
    test_output=$(cd /project && pytest -q "$candidate" 2>&1)
    local exit_code=$?

    # 5. If the run succeeded, commit and exit the loop
    if [ "$exit_code" -eq 0 ]; then
      git -C /project add "$candidate"
      git -C /project commit -m "agent: generated $(basename "$candidate")"
      echo "✅ Agentic loop succeeded on turn $turn"
      break
    fi

    # 6. If we have a clear failure signal, fold it back into the next prompt
    if echo "$test_output" | grep -q "FAILED"; then
      prompt="The following code failed its tests:\n$(echo "$test_output" | head -n 40)\n\nPlease fix the code and provide only the corrected file contents."
    else
      # No test file existed; treat non‑zero exit as a syntax/runtime error
      prompt="The following code produced an error on execution:\n$test_output\n\nPlease fix the code and provide only the corrected file contents."
    fi
  done

  if [ "$turn" -eq "$max_turns" ]; then
    echo "❌ Agentic loop exhausted $max_turns turns without a passing run."
  fi
}

Key real flags and mechanics in the example above:

  • curl -s -X POST … -H "Authorization: Bearer …" -H "Content-Type: application/json" – standard flags for any JSON‑based LLM API.
  • jq -r '.choices[0].message.content // empty' – jq is a real, shipping utility; -r outputs raw strings without quotation marks.
  • pytest -q "$candidate" – pytest is a widely deployed test runner; -q suppresses per‑test output, showing only summaries.
  • git -C /project add … commit -m "…" – real git subcommands with the -C flag changing the repository directory.

Tool Execution and Command Injection

Agentic systems often need to run commands beyond test suites—formatting code, linting, migrating databases, or deploying containers. Each injected command inherits the same security context as the agent process, which creates a small but critical surface area.

A typical real‑world hook looks like this in Python, using only standard‑library and shipping binaries:

import subprocess, shlex, json, os

def run_tool(tool_name, args):
    # Deny‑list dangerous patterns; allow‑list specific binaries
    deny = {"/etc/", "$(", "`", "||", "&&", ";", "> ", "< " }
    full_cmd = shlex.join([tool_name] + args)

    for pattern in deny:
        if pattern in full_cmd:
            raise ValueError(f"Denied pattern {pattern!r} in command {full_cmd!r}")

    result = subprocess.run(
        [tool_name] + args,
        capture_output=True, text=True,
        timeout=30,  # hard bound to prevent runaway agents
    )
    return result.stdout, result.stderr, result.returncode

Real flags that appear in real tooling:

  • subprocess.run(..., timeout=30) – timeout is a real subprocess keyword that raises TimeoutExpired if the child exceeds N seconds.
  • subprocess.CalledProcessError – raised automatically when check=True is set and the child exits non‑zero; many agents forget this and silently swallow the return code.
  • shlex.join – real Python 3.8+ function that safely assembles command lines from sequences.

A silently failing pattern occurs when an agent pipes curl output directly into bash -c without quoting:


curl -s "$LLM_OUTPUT" | bash -c

If LLM_OUTPUT contains ; rm -rf /, the shell executes it. Production‑grade agents always split the fetch and exec phases, validate the payload structure (e.g., with jq), and never eval untrusted text.

Iteration and State

Stateful agentic loops must decide when to stop. Three real‑world knobs govern this:

  1. max_turns – an integer flag (or API parameter) that caps the number of iteration cycles. In the bash example above, it defaults to 5. Exceeding it without a passing test yields a “exhausted” status.
  2. temperature – a floating‑point parameter accepted by most LLM endpoints (e.g., temperature: 0.2). Lower values make the model more deterministic, which reduces surprising code changes between turns but also reduces creative fixes. Higher values increase the chance of a passing revision on the first try but introduce non‑determinism that can cause the loop to ping‑pong between two slightly different broken versions.
  3. max_tokens (or max_output_tokens) – caps the length of the model’s reply. If the generated code plus any injected test‑fix feedback exceeds this limit, the model truncates the output, often cutting off the very fix the loop needs. A common sharp edge is an agent that loops indefinitely because each iteration’s prompt is marginally too long, causing the model to drop the error description and repeat the same mistake.

Real API shapes (generic, not vendor‑specific) accept these parameters as top‑level keys in the JSON request body. The exact key names (temperature, max_tokens) are shared across many providers, but the allowed ranges and default values differ. An agent that hard‑codes temperature: 0 for every provider will hit validation errors on platforms that call the flag temp or reject 0 as “invalid”. Always read the target endpoint’s schema before baking a flag value into a loop.

Sharp Edges: What Silently Fails

  • Relative‑path drift: When an agent cds into a temporary directory to write and run code, subsequent prompts that reference "main.py" may resolve to the wrong location on the next iteration if the loop does not cd back or use absolute paths. The symptom is a test suite that passes on turn 3 but fails on turn 4 with FileNotFoundError: 'main.py': No such file or directory, even though the file was created moments before.
  • Quoting and shell escaping: Agents that construct shell commands by concatenating strings often break when the LLM outputs a path containing spaces, e.g., /home/user/my project/report.csv. The resulting cat "/home/user/my project/report.csv" gets split into cat /home/user/my project/report.csv by an unquoted shell, producing No such file or directory. Real systems mitigate this by always passing arguments as a list to subprocess.run or run_tool, never as a single space‑joined string.
  • Token‑window overflow: Each loop iteration appends the new prompt, the model’s reply, and the test output to the conversation history. After ~10–12 turns with a 4 k token model, the earliest user instruction drops off. The agent then operates without the original intent, generating code that “looks right” but solves a different problem. The failure is silent: the model returns a 200 with plausible‑looking code, the tests still run (but against stale context), and the agent reports success.
  • Credential leakage in prompts: If an agent includes environment variables or API keys in the prompt to give the model context (“use my AWS_ACCESS_KEY_ID…”), those values appear in every subsequent turn’s history, and in any logs or error reports the system writes. A common production incident is an agent that accidentally prints its own OPENAI_API_KEY when echoing the request body for debugging.
  • Non‑deterministic test outcomes: Some tests depend on wall‑clock time, random number generation, or network latency. An agent that loops until pytest exits 0 may never succeed if a flaky test’s failure rate is non‑zero. The agent reports “passed” only because it hit max_turns; the operator sees a green CI badge but the underlying test is unreliable.

Guardrails and Production Safety

  • Sandboxed execution: Run agent‑generated code in a container with a read‑only filesystem, no inbound network, and a restricted PATH. Real deployments use Docker –read-only –tmpfs /tmp or Kata containers. Never let an agent run sudo, apt install, or curl | bash in a production host context.
  • Allow‑list of permitted commands: Maintain a file like /etc/agentic/allowlist.txt containing pytest, black, flake8, docker build --no-cache. The run_tool function from the earlier section checks every request against this list. Anything not listed is rejected with a PermissionError before the child process spawns.
  • Version‑pinned dependency snapshots: After a successful run, freeze the generated code’s imports. A real pattern: pip freeze > requirements-agent.txt immediately after the first passing commit. This prevents the next loop iteration from pulling a newer library version that changes behavior.
  • Human‑in‑the‑merge gate: Even a well‑behaved agentic loop should require an explicit git merge --no-ff approval before code enters main. Automated PR reviewers (e.g., github/codeowners with a custom agent-review label) can block merges if the commit message does not contain agent: reviewed.
  • Audit logging: Every loop turn should write a line to a structured log (JSON Lines format is real and widely used) containing turn, prompt_hash, model_name, exit_code, test_output_hash, and decision (continue/stop). This makes post‑mortem debugging of “why did the agent stop at turn 7?” trivial, and it satisfies most SOC‑2‑style compliance requirements without adding a separate business‑value section.

Moving Between Platforms

The mechanics above—HTTP request shape, jq parsing, pytest execution, git versioning, and sandboxed subprocess invocation—are not tied to any single product. They map directly to:

  • Any LLM API that accepts a JSON chat completion body with model, messages, temperature, and max_tokens.
  • Any test runner that accepts a file path and returns a non‑zero exit code on failure.
  • Any version‑control system with add, commit, and push subcommands.
  • Any container runtime that supports --read-only and tmpfs flags.

A reader can take the bash function, the Python run_tool helper, and the iteration logic, then replace the curl endpoint URL and jq path with the conventions of their own LLM provider, swap pytest for go test, npm test, or pytest‑compatible runners, and adjust the allow‑list to match their organization’s permitted binaries. The pattern remains the same; only the surface‑level identifiers change.

This page was rewritten on 14 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.