Agent definitions

Each agent is configured under [agents.<name>]. Five agents ship by default: claude, codex, opencode, cursor, and agy.

  [agents.claude]
command        = "claude"
args           = ["--session-id", "{agent_session_id}"]
resume_args    = ["--resume", "{agent_session_id}"]
fork_args      = ["--resume", "{fork_source_agent_session_id}", "--fork-session", "--session-id", "{agent_session_id}"]
non_interactive_args = []       # empty: keep the agent's native approval TUI (see below)
env            = {}             # extra environment variables
idle_timeout   = ""             # auto-stop after idle (default: 1h when resume_args is set, 0 otherwise)
inject_prompt  = true           # inject agent_prompt into the session
prompt_injection = ""           # how to inject: append_system_prompt | cursor_rules | developer_instructions | none
validate_model = ""             # command to validate --model values
headless_capable = false        # agent can run in headless (stream-json) mode (experimental)
add_dir_args   = ["--add-dir", "{dir}"]  # flag for granting an extra directory (see Includes)
headless_args  = []             # argv prefix prepended in headless mode (see below)

[agents.claude.input]
mouse_wheel_policy = "respect_terminal_modes" # off | respect_terminal_modes | always

[agents.claude.info]
# Optional provider info commands, run as: <command> <args...>
# model = ["--list-models"]
# version = ["--version"]
  

headless_capable marks whether an agent supports headless mode. Only Claude supports it in v1; you can’t ask a session to go headless on an agent that isn’t capable.

Every agent-specific flag graith appends is defined here — a custom agent can adopt (or drop) each pattern from config alone, without waiting on a graith release:

  • add_dir_args — the flag template graith uses to grant the agent an extra directory (each included repo’s co-located worktree). It is expanded once per directory with {dir} bound to that path. Leave it unset for an agent whose CLI has no such flag; those agents rely on the GRAITH_INCLUDE_*_PATH environment variables instead.
  • non_interactive_args — optional argv prepended on every create, resume, and fork. It is empty by default for every bundled agent, so each keeps its own approval TUI (and, for Codex, its own sandbox) out of the box; Graith treats time spent in that TUI as ordinary running state and never answers on your behalf. Set it to the agent’s unattended flag(s) — e.g. ["--dangerously-skip-permissions"] for Claude, ["--ask-for-approval", "never", "--sandbox", "danger-full-access"] for Codex, ["--force"] for Cursor, ["--auto"] for OpenCode — to run without those prompts. Doing so disables the agent’s native safeguards, so only enable it behind a boundary you control (Graith’s [sandbox], an external sandbox, or a VM). Graith does not provide a semantic shell-command policy; configure an agent-native hook or external policy tool directly if needed.
  • headless_args — the control-channel argv prefix graith prepends when launching the agent in headless mode; the agent’s own args follow it. Claude’s default is ["-p", "--output-format", "stream-json", "--input-format", "stream-json", "--verbose"].
  • option_args — conditional flag groups appended on every launch. Each group is emitted only when its when template variable is set, so an unset option leaves the agent’s own default untouched (see Conditional option flags).
  • info — provider-neutral info keys mapped to agent-native argv fragments (see Provider info commands).
  • input — per-agent terminal input overrides. In v1 this controls mouse_wheel_policy and semantic wheel gesture bindings; see TUI & input.

inject_prompt is the on/off switch for prompt injection; prompt_injection selects the mechanism. When prompt_injection is empty (the default), graith picks the mechanism from the agent name — claudeappend_system_prompt, cursorcursor_rules, codexdeveloper_instructions, and any other name → none. Set it explicitly to override that mapping, or — most usefully — to give a custom agent a mechanism it wouldn’t otherwise get. The values are:

ValueMechanism
append_system_promptPass the prompt via Claude’s --append-system-prompt flag
cursor_rulesWrite the prompt to .cursor/rules/graith.mdc in the worktree (Cursor)
developer_instructionsPass the prompt as Codex’s -c developer_instructions=... override
noneDo not inject a prompt

An unknown value is rejected at config load. Both inject_prompt and prompt_injection apply to ordinary sessions and the orchestrator alike: a Codex, Cursor, or custom orchestrator agent gets the right mechanism instead of an unsupported Claude flag, and inject_prompt = false opts the orchestrator out entirely — it launches with no injected role prompt and no Cursor rule file, just like an ordinary session.

Template variables

These are substituted in args, resume_args, fork_args, non_interactive_args, and headless_args:

VariableExpands to
{agent_session_id}UUID for the agent session (used for --session-id / --resume)
{session_id}Internal graith session ID
{session_name}Human-readable session name
{username}github_username, or discovered GitHub username, or literal "user"
{worktree_path}Absolute path to the session worktree
{model}Model passed via gr new --model (empty if not set)
{fork_source_agent_session_id}Agent session ID of the fork source (empty if not a fork)

Only {username} is available in branch_prefix.

Two more variables are scoped to specific fields. {dir} is available only in add_dir_args, bound to each granted directory in turn. The Codex option values — {profile}, {reasoning_effort}, {service_tier}, and {web_search} (a boolean rendering as true/empty) — are available in option_args, alongside {model}.

Info commands use the daemon-created probe identity only: {agent_session_id}, {session_id}, {session_name}, and {worktree_path}. They do not receive the session-launch-only variables such as {model}, {username}, or {fork_source_agent_session_id}.

Conditional option flags

option_args moves per-session choices (Codex’s --model, --profile, reasoning-effort, service-tier, and --search) into config, so a custom agent can define its own. Each group lists the argv to append and a when template variable that gates it — the group is emitted only when that variable resolves to a non-empty value (true for a boolean such as web_search). An empty when emits the group unconditionally. Non-interactive launch flags belong in non_interactive_args, not here.

  [[agents.codex.option_args]]
when = "model"                     # emit only when a model is set
args = ["--model", "{model}"]

[[agents.codex.option_args]]
when = "reasoning_effort"          # Codex has no flag for this, so ride -c
args = ["-c", "model_reasoning_effort={reasoning_effort}"]

[[agents.codex.option_args]]
when = "web_search"                # boolean: emitted when true
args = ["--search"]
  

This is why an unset option can’t just be a {model} template inside args: an empty model would expand to a literal --model "". The groups are appended after the base args on create, resume, and fork alike. A when naming an unknown template variable, or a group with no args, is rejected at config load.

Provider info commands

[agents.<name>.info] maps stable info keys to the agent-native arguments that query them. gr agent info <name> runs all configured keys; gr agent info <name> <key> runs one key. The daemon combines the agent’s configured command with the selected info argv and runs it in a daemon-owned provider context with the agent’s optional Graith sandbox. It does not use the caller’s shell PATH and does not return the configured environment in the response. The probe environment starts from a small daemon-side allowlist (PATH, HOME, SHELL, TERM, locale variables, temp-directory variables, and XDG config, data, and cache paths), then applies the agent’s explicit env map and Graith’s own GRAITH_* probe markers. Auth/socket variables such as GRAITH_TOKEN, SSH_AUTH_SOCK, and XDG_RUNTIME_DIR are not inherited from the caller environment.

  [agent_info]
cache_ttl = "1h"  # default; "0" disables the daemon memory cache

[agents.cursor]
command = "agent"
args = ["--yolo", "--model", "{model}"]
resume_args = ["--resume", "{agent_session_id}", "--yolo"]

[agents.cursor.info]
version = ["-v"]  # legacy argv-array form

[agents.cursor.info.model]
args = ["--list-models"]
format = "model_list"
cache_ttl = "30m"  # optional per-key override
  

The short key = ["arg", "..."] form is still accepted and is equivalent to format = "raw". Use the table form when a key needs a formatter or a per-key cache duration. Built-in formats are:

FormatJSON fields
rawraw stdout and stderr only
lineslines, split from stdout without line endings
model_listmodels, parsed from id - description stdout lines

Claude and Codex ship with a version probe by default:

  [agents.claude.info]
version = ["--version"]

[agents.codex.info]
version = ["--version"]
  

Info commands are for provider catalog and version probes, not session launch. They use only the selected info argv, not the normal args, resume_args, option_args, prompt injection, hooks, or included-repo flags. Successful results are cached in daemon memory per agent, key, and effective command configuration. The default cache TTL is one hour from [agent_info].cache_ttl; set it to "0" to disable caching, or set cache_ttl on one info table to override that key while global caching is enabled. The cache is not persisted, so gr daemon restart starts empty. gr agent info --refresh runs the provider command and updates the cache on success. --no-cache runs the provider command without reading or writing the cache. The two flags are mutually exclusive. Expired entries are not returned; if the provider fails after expiry, the failure is returned and the stale data remains unserved. Concurrent misses or refreshes for the same agent/key share one provider command where possible.

Each command is bounded by a 30 second daemon timeout. Unknown agents, unknown keys, and agents without info configuration fail the request. Timeouts and provider command failures are returned on their specific result with non-zero exit status and an error field; the CLI still exits non-zero after rendering those results. Captured stdout and stderr are capped at 1 MiB each; with --json, stdout, stderr, structured fields, cache metadata, truncation flags, warnings, exit code, and per-result errors are returned as separate fields.

Per-agent sandbox

  [agents.claude.sandbox]
read_dirs  = ["~/.claude"]
write_dirs = ["~/.claude"]
write_files = ["~/.claude.json", "~/.claude.json.lock", "~/.claude.lock"]  # login file (read+write)
features   = ["clipboard"]
  

Features, directories, and files (read_files/write_files, for single files that can’t be a directory grant without over-sharing — e.g. Claude’s ~/.claude.json login file) merge with the global sandbox config. Per-agent settings can choose a backend, add grants, or explicitly disable Graith’s sandbox for that agent. Disabled sessions start with a warning; gr doctor reports the missing Graith boundary. See the Sandbox page for file grants.

Custom agents

Define additional agents beyond the built-in five:

  [agents.my-agent]
command     = "/usr/local/bin/my-agent"
args        = ["--session", "{agent_session_id}", "--model", "{model}"]
resume_args = ["--resume", "{agent_session_id}"]
env         = { MY_CONFIG = "production" }
idle_timeout = "2h"
prompt_injection = "append_system_prompt"  # how to inject agent_prompt (else the prompt is skipped)

[agents.my-agent.info]
version = ["--version"]

[agents.my-agent.sandbox]
read_dirs  = ["~/.my-agent"]
write_dirs = ["~/.my-agent"]
  

Use with gr new my-task --agent my-agent. Since a custom agent’s name matches none of the built-ins, set prompt_injection if you want it to receive agent_prompt — otherwise the name-based default is none and no prompt is injected.

Agent-owned native integrations

Graith preserves ordinary configured agent arguments but does not configure, inject, supervise, proxy, inspect, or secure unrelated integrations supplied by an agent runtime. Keep those settings in the runtime’s native files or flags, outside Graith’s schema. For browser automation in a sandboxed session, use agent-browser.

Repository configuration

Per-repo settings:

  [[repos]]
path             = "~/Code/my-project"
allow_concurrent = false  # allow multiple in-place sessions
singleton        = false  # allow only one session at a time
includes         = ["~/Code/shared-lib"]  # include other repos in the session
  

singleton and allow_concurrent are mutually exclusive.

Includes

When includes is set, the daemon creates worktrees for the included repos alongside the main worktree. The included repo paths are exposed as environment variables:

  GRAITH_INCLUDE_<BASENAME>_PATH=/path/to/included/worktree
  

The basename is uppercased, with - and . replaced by _. For example, ~/Code/shared-lib becomes GRAITH_INCLUDE_SHARED_LIB_PATH.

On launch, the daemon also grants each included worktree to the agent via its add_dir_args flag, so it can reach the sibling worktrees without an extra grant prompt. This applies only to agents that define add_dir_argsclaude, codex, and cursor ship with ["--add-dir", "{dir}"]; other agents rely on the environment variables above. The flags are re-added on resume and fork, so they survive restarts.

Config path rewriting

Relative references between repos (../shared-lib) resolve correctly because the worktrees are arranged as siblings. Absolute references (~/Code/shared-lib or /Users/you/Code/shared-lib) don’t — they still point at your main checkout, not the session’s worktree.

To help, after creating the worktrees (on both create and fork) the daemon rewrites known orchestrator config files in each worktree, substituting each source repo path with its session worktree path:

  • .env.local
  • docker-compose.override.yml

Both the resolved absolute path and its ~/-relative form are matched, at path boundaries only — so ~/Code/grafana is rewritten while a sibling like ~/Code/grafana-enterprise (or grafana.bak, grafana@next) is left untouched. A path that continues into the repo (~/Code/grafana/conf) keeps its suffix, and when one included repo is nested under another the more specific path wins.

Only files present in the worktree are touched; a gitignored file (absent from a fresh checkout) is skipped, and the GRAITH_INCLUDE_*_PATH env vars remain the mitigation there. Symlinks are never read or replaced — a config symlink could otherwise pull an external file’s contents into the worktree. When a tracked file is rewritten it’s marked --skip-worktree, so the session-specific path isn’t reported as a change or committed by accident. Rewriting is best-effort — a read or write failure is logged, never fatal to session creation.

Validation rules:

  • A repo cannot include itself
  • Included repo basenames must be unique
  • Environment variable names derived from basenames must not collide

Default agent configurations

Every built-in agent also sets the shared lifecycle and prompt-delivery defaults explicitly, so they show up in gr config show: idle_timeout = "1h", inject_prompt = true, and pre_trust_workspace = true. Each also sets prompt_injection to its native mechanism — append_system_prompt (Claude), developer_instructions (Codex), cursor_rules (Cursor), and none (OpenCode, Agy) — and sets [agents.<name>.input] mouse_wheel_policy = "respect_terminal_modes" so wheel-up can enter scroll mode only when the child has not claimed wheel input. The blocks below omit these shared keys and show only the per-agent command, args, and resume/fork settings.

Claude

  [agents.claude]
command      = "claude"
non_interactive_args = []   # keep Claude's approval TUI; set ["--dangerously-skip-permissions"] to run unattended
args         = ["--session-id", "{agent_session_id}"]
resume_args  = ["--resume", "{agent_session_id}"]
fork_args    = ["--resume", "{fork_source_agent_session_id}", "--fork-session", "--session-id", "{agent_session_id}"]
add_dir_args = ["--add-dir", "{dir}"]
headless_args = ["-p", "--output-format", "stream-json", "--input-format", "stream-json", "--verbose"]

[agents.claude.info]
version = ["--version"]
  

Codex

  [agents.codex]
command      = "codex"
# Empty by default: Codex keeps its own approvals AND its own sandbox. Setting
# these flags disables both, so only do it behind a guaranteed outer boundary.
non_interactive_args = []   # e.g. ["--ask-for-approval", "never", "--sandbox", "danger-full-access"] to run unattended
args         = []
resume_args  = ["resume", "{agent_session_id}"]
fork_args    = ["fork", "{fork_source_agent_session_id}"]
add_dir_args = ["--add-dir", "{dir}"]

# The model and typed Codex options (--model, --profile, reasoning effort,
# service tier, --search) are emitted via option_args groups
# gated on the matching template variable. See "Conditional option flags" above.
[[agents.codex.option_args]]
when = "model"
args = ["--model", "{model}"]
# … profile, reasoning_effort, service_tier, web_search …

[agents.codex.info]
version = ["--version"]
  

OpenCode

  [agents.opencode]
command     = "opencode"
non_interactive_args = []   # keep OpenCode's prompts; set ["--auto"] to run unattended
args        = []
resume_args = ["--session", "{agent_session_id}"]
  

OpenCode’s TUI keeps its native prompts by default. Set non_interactive_args = ["--auto"] to approve requests that would otherwise ask; explicit OpenCode deny rules still apply.

Cursor

  [agents.cursor]
command        = "agent"
non_interactive_args = []   # keep Cursor's prompts; set ["--force"] to run unattended
args           = []
resume_args    = ["resume"]
validate_model = "agent --list-models"
add_dir_args   = ["--add-dir", "{dir}"]

[agents.cursor.info]
model = ["--list-models"]
version = ["-v"]
  

When lifecycle hooks are enabled, graith publishes .cursor/hooks.json in the worktree and records an ownership marker in its per-session data. Concurrent sessions that intentionally share a worktree also share this file when their generated hook definitions are identical. The file remains until the last owning session is deleted. A session that requires a different definition fails before launch while another owner remains.

An existing file graith doesn’t own fails the session launch rather than being overwritten; move it aside before retrying. Cleanup removes only the unchanged file object graith published, so a file you edit or replace is preserved. This protection applies equally to ordinary and --in-place sessions and survives a daemon restart.

Agy

  [agents.agy]
command     = "agy"
non_interactive_args = []   # keep Agy's prompts; set ["--dangerously-skip-permissions"] to run unattended
args        = []
resume_args = ["--conversation", "{agent_session_id}"]