Vibe Code Textbook

Harnesses · · 1,458 words · 7 min read

Claude Code vs Codex vs Gemini CLI: what an agent may run

The permission models of Claude Code, Codex CLI, Gemini CLI, the OpenHands SDK, and Omnigent, read from their docs on 2026-09-05, with a working settings file.

permissions sandbox claude code codex gemini cli

You get the permission model of five harnesses read from their own documentation on one day, a table that puts them side by side, the exact rule syntax each one uses, and a complete Claude Code settings file you can drop into a repository. Versions matter here because these pages change monthly: Claude Code was at v2.1.261 and Codex CLI at rust-v0.153.4 when I read them, both released 2026-09-04.

Two questions every harness answers

A permission model answers two separate questions. First, which actions need a human before they run. Second, what an action can physically reach once it runs. The first is a policy question and is usually configured with modes and rules. The second is an enforcement question and is usually a sandbox. The Claude Code permissions page separates them explicitly: permissions "control which tools Claude Code can use and which files or domains it can access", while sandboxing "provides OS-level enforcement that restricts the Bash tool's filesystem and network access", and the page recommends both "for defense-in-depth". Keep the two questions apart and the five harnesses become easy to compare.

Claude Code

Claude Code has six permission modes. The permission-modes page lists them with what runs without asking: default (labelled Manual in the interface) runs "Reads only"; acceptEdits adds "file edits, and common filesystem commands (mkdir, touch, mv, cp, etc.)"; plan runs reads "plus classifier-approved commands when auto mode is available"; auto runs "Everything, with background safety checks"; dontAsk runs "Only pre-approved tools"; and bypassPermissions runs "Everything". The same page says the built-in starting mode on Pro, Max, and Team plans is auto mode, where "a second model, the classifier, reviews actions instead of you", and that bypassPermissions is for "Isolated containers and VMs only".

On top of the mode sit rules in three lists. The permissions page: "Rules are evaluated in order: deny, then ask, then allow. The first match in that order determines the outcome, and rule specificity doesn't change the order." A rule is Tool or Tool(specifier). The documented forms include Bash(npm run build) for an exact command, Bash(git diff *) where "the trailing * enables prefix matching", Read(./.env), and WebFetch(domain:example.com). Deny and ask rules can also match a scalar input parameter, such as Agent(isolation:worktree).

Two details of Bash matching are easy to get wrong. Compound commands are split on &&, ||, ;, |, |&, &, and newlines, and "A rule must match each subcommand independently", so Bash(safe-cmd *) does not approve safe-cmd && other-cmd. And a built-in read-only set, including ls, cat, grep, find, wc, diff, and read-only forms of git, runs without a prompt in every mode and "is not configurable". When you answer a prompt with "Yes, and don't ask again", the rule is written to .claude/settings.local.json at the repository root.

The sandbox is separate. The sandboxing page says it "runs on macOS, Linux, and WSL2. Native Windows is not supported", uses Seatbelt on macOS and bubblewrap on Linux, and, with autoAllowBashIfSandboxed at its default of true, "sandboxed Bash commands run without prompting even if your permissions include a bare Bash ask rule".

Codex CLI

Codex has one knob for each question and names them accordingly. The configuration reference lists approval_policy with the values untrusted | on-request | never plus a granular object, and sandbox_mode with read-only | workspace-write | danger-full-access. The approvals page describes untrusted as running "safe read operations automatically but requires approval for state-mutating commands", on-request as asking "when actions require it (sandbox escalations, network access, etc.)", and never as disabling prompts so Codex "operates within sandbox constraints autonomously". The reference notes that on-failure is deprecated.

The sandbox defaults are strict. The approvals page says that in workspace-write the .git, .agents, and .codex directories "remain read-only", and that "By default, the agent runs with network access turned off". The [sandbox_workspace_write] table has writable_roots, network_access, exclude_tmpdir_env_var, and exclude_slash_tmp. Enforcement is OS-level: macOS "uses Seatbelt policies and runs commands using sandbox-exec", Linux "uses bwrap plus seccomp by default", and Windows uses the Linux path under WSL2 or a native sandbox otherwise. The escape hatch is --yolo, an alias for --dangerously-bypass-approvals-and-sandbox.

A config built only from documented keys:

#~/.codex/config.toml (or .codex/config.toml in a trusted project)
model = "gpt-5.6"
approval_policy = "on-request"
sandbox_mode = "workspace-write"

[sandbox_workspace_write]
network_access = false
writable_roots = ["/home/dev/project"]
exclude_slash_tmp = true

Gemini CLI

Gemini CLI puts the approval question under general.defaultApprovalMode in settings.json, with "default" (prompts), "auto_edit" (auto-approves edit tools), and "plan" (read-only). The configuration page adds one rule I have not seen elsewhere: "YOLO mode (auto-approve all actions) can only be enabled via command line (--yolo or --approval-mode=yolo)". You cannot make the all-approve mode the persistent default from a settings file.

The sandbox is configured with tools.sandbox, which accepts "a boolean to enable or disable the sandbox, provide a string path to a sandbox profile, or specify an explicit sandbox command (e.g., "docker", "podman", "lxc", "windows-native")", or the GEMINI_SANDBOX environment variable. Settings load from four locations with system settings taking the highest precedence over project and user files.

OpenHands SDK

OpenHands treats approval as a policy object on the conversation. The security guide lists three: AlwaysConfirm() "Require approval for all actions", NeverConfirm() "Execute all actions without approval", and ConfirmRisky() "Only require approval for risky actions (requires security analyzer)". The analyzer it ships is an LLM security analyzer that "Reviews each action before execution" and classifies actions as LOW, MEDIUM, HIGH, or UNKNOWN risk. When an action needs approval, the agent step sets the conversation status to waiting and the caller either approves or calls conversation.reject_pending_actions().

The reach question is answered by the workspace type rather than by a flag: a local workspace runs commands with subprocess.run on the host, a Docker or remote workspace runs them through an agent server in a container. That is covered in /posts/openhands-architecture-explained.html.

Omnigent

Omnigent is a layer over other harnesses, so its policies apply to whichever agent runs underneath. Its README at commit 83cc3e7113df says "Policies decide what an agent may do: run shell commands, edit files, spend tokens", and shows three built-ins: ask_on_os_tools, max_tool_calls_per_session with a limit, and cost_budget with max_cost_usd and ask_thresholds_usd. The spend cap is the interesting one; none of the other four expresses a dollar budget as a permission rule, though Claude Code has --max-budget-usd for non-interactive runs.

Side by side

Harness Who approves Granularity of rules Reach enforcement Default network
Claude Code you, or a classifier in auto mode per tool, per command prefix, per path, per domain Seatbelt / bubblewrap sandbox for Bash (opt-in) permissions only unless sandbox on
Codex CLI you, per approval_policy policy plus sandbox mode; granular categories Seatbelt / bwrap+seccomp, always on unless bypassed off in workspace-write
Gemini CLI you, per approval mode mode-level Docker, Podman, or profile via tools.sandbox not stated on the page I read
OpenHands SDK your code, via a policy object per action, by analyzer risk level workspace type (local process vs container) depends on workspace
Omnigent policies over any harness per tool call, per session count, per dollar delegated to the wrapped harness plus bwrap/seatbelt terminals depends on harness

The shared shape is clear. Everyone has a "read only" floor, an "edits without asking" middle, and an "everything" ceiling that the docs tell you to reserve for containers. The differences are in how rules are expressed: Claude Code's are the most fine-grained, Codex's are the most conservative by default, Gemini's deliberately keep the ceiling off the config file, and OpenHands moves the decision into your own code.

The shipped settings file

The code box carries coding-agent-permission-models-compared.json, a complete .claude/settings.json. Its shape:

{
  "permissions": {
    "defaultMode": "acceptEdits",
    "allow": ["Read", "Bash(python -m pytest *)", "Bash(git diff *)", "Bash(git commit *)"],
    "ask": ["Bash(git push *)", "Bash(pip install *)", "Edit(./migrations/**)"],
    "deny": ["Read(./.env)", "Bash(rm -rf *)", "Bash(curl *)", "WebSearch"]
  },
  "sandbox": {"enabled": true, "autoAllowBashIfSandboxed": true}
}

The full file has twelve allow rules, five ask rules, and eight deny rules. I validated it in this session with python -m json.tool, which printed the file back without error, and a short Python check that counted the lists. Two choices in it deserve a sentence. Denying curl and wget while allowing WebFetch(domain:docs.python.org) follows the permissions page's own advice, which warns that Bash patterns constraining URLs "are fragile" and suggests denying the network tools and using WebFetch domain rules instead. Putting defaultMode at acceptEdits rather than auto is deliberate: the settings docs say auto and bypassPermissions "don't take effect from project or local settings" and must be set in user or managed settings or passed as a flag.

What I did not verify

I read each behaviour from the vendor page named in the sources on 2026-09-05 and did not exercise the prompts themselves in this session. The Gemini CLI page I read did not state a network default, so the table says so rather than guessing. Codex's granular approval object has five boolean fields in the reference; I did not test how they combine.

Code and data

Sources

  1. Anthropic, "Configure permissions" (Claude Code docs, read 2026-09-05)
  2. Anthropic, "Choose a permission mode" (Claude Code docs, read 2026-09-05)
  3. Anthropic, "Configure the sandboxed Bash tool" (Claude Code docs, read 2026-09-05)
  4. OpenAI, "Agent approvals & security" (Codex docs, read 2026-09-05)
  5. OpenAI, "Configuration Reference" (Codex docs, read 2026-09-05)
  6. OpenAI, "Config basics" (Codex docs, read 2026-09-05)
  7. Google, "Gemini CLI configuration" (read 2026-09-05)
  8. OpenHands, "Security & Action Confirmation" (Software Agent SDK docs, read 2026-09-05)
  9. omnigent-ai/omnigent, "README at commit 83cc3e7113df" (read 2026-09-05)