Articles

How AI Coding Agents Actually Run on Your Machine

A deep technical look at how Claude Code, Codex CLI, and other coding assistants manage sessions, enforce permissions, and store state locally — and where the governance model breaks down.

AI coding assistants are usually described as "chat interfaces with repository access." That framing undersells what is actually happening. Tools like Claude Code, Codex CLI, Cursor, and Windsurf are local agent runtimes. They run as processes on a developer workstation, assemble repository context, send prompts to a model API, receive structured tool-call requests, execute those requests against the local filesystem and shell, record append-only session logs, and maintain enough state to resume work across sessions.

Diagram showing a user interacting with a local CLI or IDE agent runtime that calls a model API and executes local tools against repository files, shell commands, and network services.
The assistant UI is only the visible surface. The operating boundary is the local runtime that brokers model calls, tool execution, approvals, files, shell access, and network activity.

Once you look at the files on disk, the shape becomes concrete. A coding assistant is made of five pieces:

  1. A model connection that sends prompts, tool definitions, repository context, and previous conversation turns to a remote API.
  2. A tool layer that turns model requests like "read this file" or "run this command" into real local actions.
  3. A session store that records every message, tool call, tool result, token count, and metadata to disk.
  4. A permission system that decides whether a requested action is allowed, denied, or requires interactive user approval.
  5. A host environment — files, shell state, network, credentials, Git remotes, package managers, MCP servers — that determines what the assistant can actually reach.

That architecture is powerful. It also creates an uncomfortable truth for security teams: the assistant is not just producing code. It is operating a developer workstation.

The Runtime Loop

Every coding assistant runs a variant of the same loop:

user intent
  -> local context assembly (files, git state, project instructions)
  -> model API request (prompt + tool definitions + prior turns)
  -> model response: text or structured tool_use call
  -> local tool execution (read, write, bash, grep, web fetch, ...)
  -> tool result appended to conversation transcript
  -> next model API request
  -> ... (repeat until the model emits a final text response)

The model never directly touches your files or runs commands. It emits structured tool calls. The local process — the CLI binary or IDE extension — executes those calls, captures the result, and feeds it back into the conversation. This is why the tool runtime matters as much as the model itself. The security boundary is not "what did the model say?" It is "what did the local process actually do after the model asked?"

Claude Code records tool use in Anthropic-style content blocks:

{
  "type": "tool_use",
  "id": "toolu_01ABC...",
  "name": "Bash",
  "input": { "command": "cargo test --release", "timeout": 120000 }
}

The result comes back as a tool_result block keyed by the same id. The tool vocabulary includes Read, Edit, Write, Bash, Grep, Glob, Agent (for subagents), WebFetch, WebSearch, TaskCreate, and more. Each tool call is a discrete local action with observable inputs and outputs.

Codex CLI records the same pattern in OpenAI function-call form:

{
  "type": "function_call",
  "name": "exec_command",
  "arguments": "{\"cmd\":[\"rg\",\"-n\",\"PolicyStore\",\"src\"],\"workdir\":\"/repo\"}",
  "call_id": "call_xYz..."
}

The result is a later function_call_output item with the same call_id. Different products serialize the loop differently, but the operating model is identical: model output becomes a typed request to a local runtime, and the runtime decides whether and how to execute it.

Where Claude Code Stores State

Claude Code stores most of its operational state in flat files under ~/.claude/. The directory structure reveals how sessions, permissions, and project context are actually managed:

~/.claude/
  settings.json              # Global permission rules, model, plugins
  .credentials.json          # OAuth tokens, service tier
  history.jsonl              # Index: prompt -> project path + session ID
  projects/
    <escaped-cwd>/           # One dir per working directory (path-encoded)
      <session-uuid>.jsonl   # Full conversation transcript
      memory/                # Persistent memory files (per-project)
      CLAUDE.md              # Project instructions (loaded into context)
  sessions/
    <pid>.json               # Active session metadata (pid, cwd, start time)
  file-history/
    <session-uuid>/
      <content-hash>@v1      # Content-addressed file snapshots before edits
  tasks/                     # Task tracking state (per-session)
  todos/                     # Agent-specific task metadata
  session-env/               # Shell environment snapshots
  shell-snapshots/           # Shell state captured at session start
  plugins/                   # Plugin system (hooks, LSP, MCP servers)
    marketplaces/
    installed_plugins.json

The lightweight index is history.jsonl — each line maps a user prompt to a project path and session ID. The full transcript lives in projects/<escaped-cwd>/<session-uuid>.jsonl, where each line is a chronological JSON object with fields like type, uuid, parentUuid, timestamp, isSidechain, and message.

The parentUuid and isSidechain fields are significant. They reveal that a session is not a linear chat. Claude Code can spawn subsidiary agents, delegate to internal subagents via the Agent tool, or run sidechain reasoning. The transcript preserves lineage so the conversation graph can be reconstructed — but understanding it requires parsing the tree, not just reading lines sequentially.

Claude Code also keeps file backups in file-history/<session-uuid>/. These are content-addressed snapshots of files taken before edits. Paired with file-history-snapshot records in the transcript, they can reconstruct the before-and-after state of every file change in a session. That is useful for debugging. It is also a reminder that the assistant's local data directory may contain source code, prompts, environment captures, and historical file contents.

Claude Code Permissions

Project-level permissions live in .claude/settings.local.json:

{
  "permissions": {
    "allow": [
      "Bash(cargo:*)",
      "Read(/repo/**)",
      "Bash(sqlite3:*)",
      "WebFetch(domain:github.com)"
    ],
    "deny": [],
    "ask": []
  }
}

Global user-level permissions in ~/.claude/settings.json follow the same structure but apply across all projects. Permission rules use pattern matching — Bash(cargo:*) allows any cargo command, Read(/repo/**) allows reading any file under /repo. Wildcards, domain restrictions, and prefix patterns are supported.

Claude Code also supports a hooks system via plugins. Hooks fire on events like PreToolUse, PostToolUse, UserPromptSubmit, and Stop. A PreToolUse hook receives the tool name and input as JSON on stdin and can return a permission decision (allow, deny, or ask). This is a powerful extensibility point — but hooks are local to the machine, configured per-user or per-project, and their presence is not centrally auditable.

The practical result: if a developer repeatedly approves broad command patterns, those patterns accumulate as policy. The assistant's operating envelope widens silently over time, one approval at a time.

Where Codex Stores State

Codex CLI uses ~/.codex/. The directory combines JSONL transcripts with SQLite databases for richer structured queries:

~/.codex/
  config.toml               # Model preferences, per-project trust levels
  auth.json                  # OAuth tokens (file mode 0600)
  rules/
    default.rules            # Persisted approval decisions as prefix rules
  history.jsonl              # Global interaction history
  sessions/
    <year>/<month>/<day>/
      rollout-<iso-ts>-<uuid>.jsonl   # Full session transcript
  state_5.sqlite             # Threads, tools, jobs, spawn edges, sandbox policy
  logs_2.sqlite              # Logging database
  goals_1.sqlite             # Goal/task tracking
  memories/                  # User-created notes
  skills/                    # Skill definitions (system and custom)
  plugins/                   # Plugin cache and remote installations
  cache/                     # Application and tool caching

Codex Trust and Approvals

The config.toml records model preferences and per-project trust:

personality = "pragmatic"
model = "gpt-5.5"
model_reasoning_effort = "xhigh"

[projects."/home/user/code/my-app"]
trust_level = "trusted"

[projects."/home/user/code/infra"]
trust_level = "trusted"

Trust is not abstract — it is bound to absolute filesystem paths. Each trusted project directory gets a different posture from the CLI.

Codex's rules/default.rules persists approval decisions as prefix-matching rules:

prefix_rule(pattern=["rg"], decision="allow")
prefix_rule(pattern=["sed"], decision="allow")
prefix_rule(pattern=["cargo", "check"], decision="allow")
prefix_rule(pattern=["bun", "run", "build"], decision="allow")

This format is deliberately simple. A tool call is either matched by a previously approved prefix, rejected, or routed to the user for interactive approval. For an individual developer, this is a reasonable ergonomics tradeoff. For an organization, prefix rules are only the beginning of a policy story — you still need to know who approved them, when they were used, what they affected, and whether they should apply across machines.

Codex Session Structure

The SQLite database state_5.sqlite adds structure that plain JSONL files cannot provide. Its threads table records session IDs, rollout paths, working directories, model provider, model name, token usage, Git branch, Git SHA, sandbox policy, approval mode, and a generated title. Other tables include dynamic_tools, thread_spawn_edges, agent_jobs, and agent_job_items.

That is the clearest sign that "a chat" is the wrong abstraction. Codex internally models sessions as threads with tools, jobs, child threads, and resumable state. A sandbox_policy field per thread means sandboxing is a property of a specific session — it can change between runs, and it directly alters the assistant's real authority over the machine.

Sessions, Subagents, and Lineage

Subagents are not magic. They are nested tool calls, sidechain transcript records, or child sessions.

Claude Code exposes an Agent tool that spawns a subagent with its own tool access. Lineage is tracked through parentUuid and isSidechain fields in transcript records, plus task and todo artifacts keyed by session and agent IDs. The subagent may look separate in the UI, but in the data it is a branch in a transcript DAG.

Codex records parent-child relationships more explicitly. The thread_spawn_edges table links parent threads to children. The agent_jobs and agent_job_items tables represent batched parallel work, with assigned thread IDs, attempt counts, result JSON, and completion status. A parallel coding task is not a long prompt — it is a small job system with its own lifecycle.

This distinction matters for audit. A useful audit log cannot stop at the top-level user prompt. It needs to answer:

Which child thread, subagent, or sidechain performed the action?
Which tool call requested the action, and what were its inputs?
Which local approval rule or sandbox policy allowed it?
Which files were read, written, or deleted as a result?
Which model request and response bracketed the tool execution?

Each assistant's local store contains pieces of those answers. None of them contain all the pieces in the same schema, and none of them expose that data as a unified governance surface.

How Policy Actually Gets Enforced

There are five layers of policy around a coding assistant, and they are easy to conflate:

Layer 1: Prompt policy. System instructions, CLAUDE.md project files, AGENTS.md files, and user instructions steer the model's behavior. This shapes what the model is inclined to do, but it does not enforce anything locally. A model can be told "never delete files" — but the enforcement of that rule depends on what the local runtime actually checks.

Layer 2: Tool schema. The model can only request tools that the runtime exposes. Claude Code defines Read, Edit, Write, Bash, Grep, Glob, Agent, WebFetch, and others. Codex exposes exec_command, read_file, write_file, and similar. This limits the action vocabulary — but within a tool like Bash, the command space is effectively unbounded.

Layer 3: Runtime permissioning. This is where allow, deny, and ask decisions happen. Claude Code evaluates permission rules, fires PreToolUse hooks, and prompts the user when a tool call is not covered. Codex matches commands against prefix rules and approval modes. This layer mediates individual tool calls — but it operates per-user, per-machine, and each product's format is different.

Layer 4: Sandboxing. Codex stores a sandbox_policy per thread that can restrict filesystem writes, network access, or shell execution scope. Claude Code supports a --dangerously-skip-permissions flag (the name is instructive). These controls change the assistant's blast radius for a session — but they are session-scoped, locally configured, and not independently verified.

Layer 5: Host environment. Shell profiles, proxy variables, CA trust stores, Git remotes, local credentials, .env files, Docker socket access, browser sessions, and MCP server configurations all shape what the assistant can reach. A model asks for Bash(curl https://internal-api.corp/deploy) — the host environment determines whether that request succeeds.

Prompt
Steers intent, not behavior
Runtime
Mediates individual actions
Sandbox
Limits session blast radius

Here is where the gap appears. These controls are designed for a single developer trying to get work done. Permission rules accumulate locally. Approvals are persisted per-machine. Sandbox settings are session properties. Hook configurations differ between projects. There is no shared control plane, no central policy distribution, no cross-tool normalization, and no independent verification that what was allowed matched what actually happened.

Supervision From the Outside

The Rye core codebase approaches this differently. Instead of relying on each assistant's internal controls, it treats assistants as processes that need external supervision. There are three key mechanisms.

Process wrapping with PTY supervision

rye wrap runs an assistant inside a pseudo-terminal. It calls openpty(), fork(), and execvp() to launch the child process (Claude Code, Codex, or any command) with proxy environment variables injected automatically:

HTTP_PROXY=http://127.0.0.1:18080
HTTPS_PROXY=http://127.0.0.1:18080
NODE_EXTRA_CA_CERTS=/path/to/rye.pem

The parent process relays all keystrokes and output between the user's terminal and the child, so the assistant's TUI works exactly as it normally does. While the session runs, the wrapper:

  • Watches the working directory recursively with inotify for file creates, modifies, deletes, and renames
  • Batches file-change telemetry and sends it to the Rye daemon every 5 seconds
  • Emits session.start and session.end events with the command, PID, working directory, exit code, and duration
  • Forwards SIGINT, SIGTERM, and SIGWINCH (terminal resize) to the child

The wrapper also recognizes a control command: typing /rye at the start of an input line runs a Rye CLI command from within the wrapped session — rye status, rye doctor, etc. — without needing a separate terminal.

Approval scanning

The wrapper scans the child's terminal output for permission approval events. It strips ANSI escape sequences and looks for patterns like:

You approved codex to run sed -n '1,240p' src/wrap.rs this time
You approved codex to always run commands that start with sed

Each detected approval is recorded as a structured approval.granted event with the agent name, command, approval type (this_time, always, or always_rule_match), the prefix pattern for "always" rules, and a generated approval ID. Subsequent one-time approvals that match an existing "always" rule are linked back to it by ID — so the audit trail shows both the moment a broad rule was created and every action that later executed under it.

This is a critical distinction. The assistant records that it was told to execute a command. The wrapper records that the user approved it. Those are not the same event, and correlating them is essential for understanding what actually happened in a session.

Network-level policy enforcement

The Rye daemon runs a local MITM proxy and an optional API shim. The proxy intercepts HTTPS traffic to configured LLM domains — OpenAI, Anthropic, Claude, Cursor, Windsurf, Gemini — using an auto-generated CA certificate. The API shim can sit directly in front of an upstream model API.

Before a request reaches the upstream API, the shim evaluates it against a policy bundle. The policy engine checks rules in order, and the first match wins:

1. DENY  if body contains "BEGIN OPENSSH PRIVATE KEY"
2. DENY  if body exceeds 2 MB
3. ALLOW if domain is openai.com, anthropic.com, claude.com, cursor.com, ...
4. REQUIRE APPROVAL if model field is missing (unknown destination)

When a rule matches Deny or RequireApproval, the shim returns a 403 Forbidden with a structured JSON body — the request never reaches the upstream API. The response includes the rule ID, version, and reason, so the block is traceable.

The policy store is not just a local config file. A gRPC policy sync client subscribes to a remote policy service using a bidirectional stream. The server can push policy snapshots, send invalidation signals, or delete policies. The client acknowledges each revision, reports its status, and falls back to periodic snapshot fetches if the stream disconnects. Policies are validated, persisted locally in SQLite, and applied atomically. That turns policy from a local preference file into a centrally managed, versioned artifact with audit trail.

Shims: always-on wrapping

For teams that want every claude or codex invocation to be supervised, Rye writes shell shims to ~/.rye/shims/:

#!/bin/sh
# Managed by Rye. Remove with `rye app unwrap claude`.
exec '/usr/local/bin/rye' app run-shim 'claude' "$@"

When ~/.rye/shims is earlier in PATH than the real binary, every claude invocation is automatically wrapped — no workflow change, no extra commands. A rye app doctor command verifies PATH ordering, shim existence, and that the underlying binary is still installed.

Ingestion: Understanding What Already Happened

Separate from real-time enforcement, a background ingester scans local assistant history files:

~/.claude/projects/**/*.jsonl     (Claude Code transcripts)
~/.codex/sessions/**/*.jsonl      (Codex CLI rollout files)
~/.cursor/**/*.jsonl              (Cursor session data)
~/.config/Cursor/User/workspaceStorage/**  (Cursor workspace state)

The ingester reads new bytes from each file using checkpointed byte offsets stored in SQLite, splits large content on JSONL line boundaries, hashes each chunk with SHA-256, skips already-uploaded chunks, and submits the new content over gRPC. The backend can then reconstruct and normalize conversations across tools.

This ingestion path is valuable, but it is fundamentally different from enforcement. Ingestion tells you what happened. The proxy, API shim, process wrapper, and policy sync are what shape what is allowed to happen.

Security programs need both. Retrospective logs are insufficient when an assistant can read secrets, run package managers, send arbitrary network requests, or edit production-adjacent code in real time. But enforcement without a good transcript is also incomplete — you cannot explain why a decision was made or what work it affected.

The Practical Gap

Modern coding assistants already have useful local controls. They prompt before risky commands. They persist approvals. They support workspace sandboxes. They keep transcripts. Some store Git metadata, token counts, child thread relationships, and dynamic tool registrations. For individual developers, that is enough to make the product usable and reasonably safe.

For teams, the missing layer is normalization and governance:

Claude Code, Codex, Cursor, and other assistants each use a different session schema, permission format, and approval mechanism. There is no shared data model.
Local approval files record that a rule exists. They do not record who created it, why, when it was last used, or whether it should apply on other machines.
Sandbox settings describe intent, but without independent telemetry — file watches, network interception — there is no proof the sandbox held.
Subagents and child threads mean a single top-level prompt can spawn a tree of actions. Reviewing only the prompt misses the majority of what happened.
Model API requests, file edits, shell approvals, and tool calls need to be correlated across event sources to reconstruct cause and effect.

This is why external tooling matters. The goal is not to stop developers from using coding agents — the productivity gains are real and accelerating. The goal is to make the agent's actual operating boundary visible, enforceable, and auditable across every assistant a team uses.

The durable control point is not inside any single model provider's transcript format. It is the layer around the workstation: the process wrapper that sees approvals and file changes, the network proxy that sees model traffic, the policy engine that can block requests before they leave the machine, and the ingestion pipeline that normalizes vendor-specific logs into a common audit trail.

That is how coding assistants actually work today. They are local agent runtimes with rich but fragmented traces, useful but siloed controls, and no shared governance model. The next step is treating those traces and controls as infrastructure — not as features of individual products, but as a layer the organization owns.