Configuration

Config Cascade

Goa uses a 6-level cascade for configuration. Each level may override values from the previous level:

# Priority (lowest → highest)
1. Embedded defaults (compiled into binary)
2. ~/.goa/config.yaml         (home/global)
3. .goa/config.yaml           (project-level)
4. .goa/config.local.yaml     (local overrides, gitignored)
5. GOA_* env vars             (environment overrides)
6. CLI flags                  (highest priority)

Merge Rules

  • Maps: Deep-merged (nested keys from higher-priority sources win)
  • Scalars: Last-write-wins (highest priority source for each key)
  • Slices: Replaced entirely (not merged) by higher-priority sources

Config File Structure

# ── Provider ──────────────────────────────────────────────────────
active_provider: local               # Active provider ID
active_model: llama-3.2-1b-instruct  # Active model name

providers:
  - id: local                        # Unique provider identifier
    name: Local LLM                  # Display name
    endpoint: http://localhost:1234/v1/chat/completions
    api_key: ""                      # Optional API key
    default_model: llama-3.2-1b-instruct
    timeout: 60s                     # Request timeout
    max_retries: 5                   # Per-provider max retries (overrides execution.retries)
    max_retry_delay: 2s              # Cap exponential backoff
    transport: sse                   # sse | websocket
    cache_retention: none            # none | short | long
    session_id: ""                   # Cache-affinity session id (WARNING: pins ALL
                                     # conversations, sub-agents, and one-shot calls to a
                                     # single provider cache key — diverging contexts that
                                     # share a key evict each other's cache, surfacing as
                                     # unexplained cache misses. Leave empty unless you
                                     # know the provider requires it; see AGENTS.md Rule 7)
    headers:                         # Custom HTTP headers
      X-Custom: value
    metadata:                        # Provider-specific metadata
      project: goa
    preferred: true                  # Auto-select this provider

    # Per-provider configuration overrides (optional).
    # These customize provider-specific behavior without code changes.
    extra:
      normalize_null_descriptions: true   # Convert "null" → null in tool schemas
      thinking_extra_body: true            # Send thinking config in extra_body
      tool_call_id_max_length: 64          # Truncate tool call IDs (0 = no limit)
      reasoning_key: "reasoning_content"   # Field name for reasoning content

    # Agentic provider identity. Usually inferred from the preset
    # or endpoint, but can be forced for compat detection.
    provider: lm-studio              # openai | anthropic | google | ...
    api: openai-completions          # openai-completions | anthropic-messages | ...

  - id: openai
    name: OpenAI
    endpoint: https://api.openai.com/v1/chat/completions
    api_key: ${OPENAI_API_KEY}       # Env var interpolation
    default_model: gpt-4o
    provider: openai
    api: openai-completions

# ── Models ────────────────────────────────────────────────────────
models:
  - id: default
    provider: local
    model: llama-3.2-1b-instruct
    temperature: 0.2
    max_tokens: 4096
    reasoning: false                 # Enable thinking/reasoning
    thinking_level: off              # off | minimal | low | medium | high | xhigh
    thinking_budget: 0               # Per-request thinking token budget
    thinking_level_native_map:       # Optional: per-key wire override (see below)
      xhigh: max
    input_types:                     # text | image
      - text
    headers:                         # Per-model HTTP headers
      X-Model-Header: "1"
    compat: '{"toolResultAsUser":true}' # Provider-specific compat JSON
    compress_output: false              # false | true

# ── Execution ──────────────────────────────────────────────────────
execution:
  mode: yolo                         # yolo | confirm | review
  retries: 2                         # Default max retries for provider stream requests (per-provider max_retries overrides)
  max_tool_repeat_total: 0          # Max identical tool calls in the entire turn (0 = disabled)
  max_tool_repeat_consecutive: 2    # Max consecutive identical tool calls (soft hint at 2, hard at limit)
  max_tool_calls: 200               # Max duplicate occurrences of the same call within the rolling window (0 = unlimited)
  tool_call_limit_reset_window: 10  # Number of recent calls inspected for the duplicate-window limit above
  token_warning: 70                  # % of budget → warning
  token_critical: 90                  # % of budget → critical
  loop_warning: 10                    # Consecutive same-tool calls before warning
  loop_interrupt: 15                 # Consecutive same-tool calls → interrupt
  activity_timeout: 2m               # Byte-idle budget per stream (event watchdog retries at 3/4: 1m30s)
  activity_warn_after: 1m20s         # Stall warning lead, must be inside the event stall
  error_threshold: 0.5               # Error rate % → mode auto-downgrade
  worktree_mode: multi_agent         # always | multi_agent
  auto_save_model: true              # Persist model changes to project .goa (home only as fallback); false = legacy home-only

# ── Context Compression ────────────────────────────────────────────
context_compression:
  enabled: true
  max_tokens: 8192                   # Target context window (0 = auto: model window)
  # Proactive fill levels (% of the effective window). Every layer is
  # OPT-IN: 0 disables it — there is no implicit engine default-on. The
  # embedded default.yaml enables only the hard ceiling (95, summarize).
  # Valid values: 0 (disabled), -1 (legacy disable spelling), or 5..100
  # in 5% steps.
  thresholds:
    soft_percent: 0                  # Early-maintenance layer (0 = off)
    trigger_percent: 0               # Trigger layer (0 = off)
    hard_percent: 95                 # Emergency ceiling (0 = off)
  # Per-layer methods. Any method works on any layer; empty = SDK default
  # (micro / tool_elision / summarize for soft/trigger/hard).
  strategies:
    soft: ""
    trigger: ""
    hard: ""                         # Options: micro | tool_elision |
                                     #          selective | hybrid | summarize
  on_context_error: true             # Reactive net: compress on context-length errors
  on_error_strategy: hybrid          # Recovery method: hybrid (elision →
                                     # selective → summarize) | summarize |
                                     # tool_elision | selective | micro
  strategy: ""                       # Legacy trigger-layer strategy alias
  preserve_recent_turns: 4           # Keep last N turns uncompressed
  micro_compaction:
    keep_recent_messages: 20         # Messages to never truncate
    min_content_tokens: 100          # Min content before truncating
    cache_miss_threshold: "1h"       # Idle time before micro triggers
    truncated_marker: "[Old tool result content cleared]"
    min_context_ratio: 0.5           # Min context usage to trigger (0.0-1.0)
  tool_result_pruning:               # CX1: pre-compaction pruner (ahead of summarize)
    threshold_chars: 8192            # Prune tool results over this many Unicode code points
    head_chars: 4096                 # Leading code points retained
    tail_chars: 1024                 # Trailing code points retained

# ── Mode ─────────────────────────────────────────────────────────
mode:
  default:
    major: coder                     # Default major mode (coder|planner|reviewer|<custom>)
    autonomy: solo                   # Default autonomy (yolo|solo|confirm|review)
    skills:                          # Default skill stack
      - test-gen
  defaults:
    coder: solo                      # Per-mode autonomy overrides
    planner: review
    reviewer: review

# Custom modes are defined as .goa/prompts/mode/<name>/definition.md files.
# See docs/PROFILES.md for the definition format.

# ── Skills ─────────────────────────────────────────────────────────
skills:
  dirs:                              # Extra skill directories
    - ~/.goa/skills
    - .goa/skills
  embedded: true
  execution_mode: subagent           # subagent | inline
  # Shipped default allowlist: the telegram skill is ON out of the box;
  # every other embedded skill stays OFF. An explicit pin in any config
  # layer REPLACES this default list and acts as a global allowlist across
  # all skill sources; until then the default list is embedded-scoped and
  # never suppresses file-based skills.
  enabled:
    - telegram
  disabled: []                       # Explicit off wins over enabled
  embedded_enabled: []               # Embedded-scoped opt-ins (home layer)
  sticky:                            # Force knowledge skills always-on (project level)
    - telegram
  sticky_off: []                     # Turn frontmatter-sticky skills back to on-demand

# ── Tools ──────────────────────────────────────────────────────────
tools:
  max_inline_bytes: 0            # Spill policy: cap on model-facing tool-result bytes
                                 # (0=disabled). Over-cap plain-text results are saved
                                 # verbatim to ~/.goa/spill/<session>/ and replaced by a
                                 # bounded head/tail preview + locator notice.
  # Optional tools toggles. Most are opt-IN (default false).
  # `clarify_disabled` is the exception: it is opt-OUT (default false),
  # so the ask_user_question tool is ENABLED by default.
  enabled:
    bg_exec: false            # background process execution
    memento: false           # thinking artifacts
    terminals: false          # Persistent terminal sessions
    ssh_bash: false          # remote shell execution
    delegate_to: false       # multi-agent delegation
    request_review: false    # multi-agent review requests
    webfetch: false           # web fetching
    python: true             # embedded gpython interpreter (opt-out)
    todo_list: true          # session todo list (opt-out; goal-linked when a goal is active)
    clarify_disabled: false  # set true to remove the ask_user_question tool
  edit:
    allow_fuzz_on_edits: true        # DEFAULT: enabled
                                     # 3-tier fuzzy matching:
                                     #   1. Exact match (byte-for-byte)
                                     #   2. Trailing whitespace normalized
                                     #   3. Full fuzzy + auto-reindent
                                     # When disabled: exact match only
  search:
    threads: 4                       # Concurrent search threads
    max_results: 20                  # Max search results per query
    exclude:                         # Directories to skip
      - node_modules
      - .git
      - vendor
      - dist
  bash:
    blocked_commands:                # Never execute these
      - rm -rf /
      - sudo
      - chmod -R
    allowed_commands: []             # Empty = allow all (except blocked)
    env_mask_patterns:               # Mask in logs
      - API_KEY
      - SECRET
      - TOKEN
      - PASSWORD
    max_output_bytes: 10485760       # Max output before truncation (10MB)
    compress_output: null            # null = auto (local=on, remote=off)
    enable_complexity_analysis: false # AST-based command complexity analysis
    max_complexity_score: 5          # Max complexity score (0=disabled)
    jail: false                      # Force project-directory jail
  python:
    timeout_seconds: 60              # Default execution timeout
  smartsearch:
    enabled: false                   # Enable BM25 relevance search (default: false)
    max_results: 20                  # Max results per query
    min_score: 0.0                   # Minimum relevance score threshold
    exclude_dirs: []                 # Additional exclusion dirs
    k1: 1.5                          # BM25 k1 parameter (term frequency saturation)
    b: 0.75                          # BM25 b parameter (length normalisation)
  terminal:
    sandbox:
      enabled: true                  # Enable sandbox (default: true)
      blocked_commands:              # Never execute these in sandbox
        - rm -rf /
        - sudo
        - curl
        - ssh
      allowed_commands: []           # Empty = allow all (except blocked)
      timeout_seconds: 60            # Default timeout
      max_output_chars: 100000       # Max output characters
      bypass_allowed: false          # Allow bypassing sandbox via config
  webfetch:
    enabled: true                    # Enable web fetching
    max_lines_default: 250           # Default max lines per fetch
    max_lines_hard: 4096             # Absolute max lines per fetch
    max_total_bytes: 20971520        # Max response body size (20MB)
    timeout_seconds: 30              # Request timeout
    user_agent: "Goa/1.0"            # Custom User-Agent header
    max_redirects: 5                 # Max redirects to follow
    allowed_schemes:                 # Allowed URL schemes
      - https
      - http
    blocked_hosts: []                # Hosts to block
    cache:
      enabled: true                  # Cache fetched pages
      dir: ""                        # Cache dir (default: .goa/cache/webfetch)
      ttl_hours: 24                  # Cache TTL
      max_entries: 1000              # Max cache entries per session
      max_bytes: 524288000           # Max cache size (500MB)
      cleanup_interval_hours: 24     # Cleanup interval
    summary:
      enabled: false                 # Enable sub-agent summarization
      sub_agent_role: "companion"    # Role for summarization
      max_input_lines: 1000          # Max lines for summarization
      default_prompt: |              # Default summarization prompt
        Summarize the following web page in 3-5 concise paragraphs...
  ssh:
    hosts:
      - id: server1
        host: server1.example.com
        port: 22
        user: deploy
        key_file: ~/.ssh/deploy_key

# ── LSP (language servers) ─────────────────────────────────────────
# `tools.enabled.lsp` is the master switch for the whole integration:
# navigation tool, edit/write/read → server linking, and background server
# spawns. Off means off — nothing spawns, nothing is touched (default: on).
# Toggle live at runtime with /tools:lsp:on | /tools:lsp:off.
# Servers spawn ASYNCHRONOUSLY per file type and never block file tools;
# diagnostics appear on edit/write for every supported language, labeled
# with their analyzer (gopls, pyright, typescript-language-server, …).
# Node servers auto-download via `npx --yes` on first use.
#
# lsp: false                  # disable LSP globally (blocks even /tools:lsp:on)
lsp:
  disable_download: false     # true = only already-installed servers (no npx/install)
  servers:                    # per-server disable/override/custom (OpenCode schema)
    pyright: { disabled: true }
    myls:
      command: ["myls", "--stdio"]
      extensions: [".my"]
      markers: ["my.toml"]
      language_id: "mylang"
# ── TUI ────────────────────────────────────────────────────────────
tui:
  theme: dark                        # dark | light | custom
  layout: default                    # default | wide | minimal | debug
  spinner: hexagon                   # hexagon (default), arc, dots, line, star, orbit, quadrant, flare, none
  animated_title: true               # Animate the terminal title with the spinner while working (default: true)
  show_timestamps: false             # Show timestamps in the chat viewport
  transparency:
    show_thinking: true              # Show reasoning/thinking blocks
    show_streaming: true             # Show streaming status indicators
    show_tool_calls: true            # Show tool execution in chat
    show_token_stats: true           # Show token usage in footer
    show_logs: false                 # Show log pane overlay
    thinking_collapsed: false        # Collapse thinking blocks by default
    thinking_pane_position: "side"   # side | bottom
    highlight_tool_input: true
  # Tool call display in the chat.
  tools:
    view: summary                    # summary (N-line preview) | full (everything)
    preview_lines: 10                # lines shown per tool block in summary mode
  # Font style toggles (all enabled by default — disable if your terminal
  # renders them poorly).
  font_styles:
    bold: true                       # **bold** → \e[1m
    italic: true                     # *italic* / _italic_ → \e[3m
    underline: true                  # links → \e[4m
    strikethrough: true              # ~~strikethrough~~ → \e[9m
  # Input history configuration.
  history:
    max_loaded: 100                  # Max history entries loaded from disk on
                                     # startup and session restore (0 = disabled)

# ── Memory ─────────────────────────────────────────────────────────
memory:
  enabled: true
  dir: .goa/memory
  auto_summarize: true
  dream:
    enabled: false                # enable memory consolidation (dream mode)
    auto: false                   # automatically run dreams after sessions
    interval: 7d                  # minimum time between auto-dreams
    min_sessions: 5               # minimum sessions since last dream
    model: ""                     # dedicated dream model (empty = active model)
    provider: ""                  # dedicated dream provider (empty = active provider)
    max_tokens: 8192              # max output tokens for the dream model
    temperature: 0.2              # dream model temperature
    output_dir: .goa/memory.dream        # review directory
    consolidated_dir: .goa/memory.consolidated  # applied memory location
    apply_after_review: false     # auto-apply without manual review

# ── Multi-Agent ────────────────────────────────────────────────────
multi_agent:
  enabled: false
  pattern: pair
  max_companion_cycles: 2
  companion_provider: ""
  companion_model: ""
  planner_model: ""
  coder_model: ""
  message_timeout: 120s
  show_inter_agent_messages: true

# ── Orchestrator ───────────────────────────────────────────────────
orchestrator:
  roles:
    orchestrator:
      model: my-model-id
    coder:
      model: my-model-id
      provider: ""                   # optional per-role provider override
      allowed_tools: [bash, edit]     # optional tool allowlist
  pool:
    max_total_agents: 8              # max concurrent agents (0 = unlimited)
    max_agents_per_model:
      my-model-id: 4                 # optional per-model cap
  defaults:
    topology: hub                    # hub | fanout | pipeline

# ── Sessions ───────────────────────────────────────────────────────
# Session logs (<project>/.goa/sessions/*.jsonl) are pruned after the retention
# window, counted from each file's last write. The window defaults to 7 days and
# is never applied to the session currently open.
sessions:
  retention:
    enabled: true                    # false keeps every session log forever
    days: 7                          # 0 = keep forever; N = keep N days

# ── Plugins ────────────────────────────────────────────────────────
plugins:
  enabled: ["*"]                     # Enable all plugins, or list specific IDs
  dirs:
    - ~/.goa/plugins
    - .goa/plugins
  bundled:                           # Disable built-in bundled plugins
    provider-quota: true             #   false → skip the provider-quota plugin

# ── Aliases ───────────────────────────────────────────────────────
aliases:
  n: session:new              # /n → /session:new
  r: session:restore          # /r → session restore picker
  # Aliases support baked-in args: "session:new" is a shortcut for
  # "/session:new". Plain command names like "session" also work.

Define custom shortcuts for any command. The key is the alias name,
the value is the target command (optionally with colon-separated args).
Aliases appear in tab completion and support the full command syntax
including subcommands and doc suffixes.

# ── Logging ────────────────────────────────────────────────────────
logging:
  level: info                        # error | warn | info | debug
  file: ""                           # Log file path (empty=stderr)

# ── Features (opt-in gates; all default OFF) ───────────────────────
features:
  # Server-side conversation compaction (Codex Phase 2b, POST
  # /responses/compact). Default off: the local compression ladder runs.
  remote_compaction: false
  # Multi-agent TUI scrollback replay on tab switch (plan T3). Default off:
  # switching agent tabs repaints only the visible window. When on, switching
  # replays the target agent's committed transcript rows into the real
  # terminal scrollback (via a dedicated ReplayRunner goroutine), so the
  # switched-to agent's history is faithful and scrollable.
  multi_agent_scrollback_replay: false

Per-Model Thinking-Level Remap (thinking_level_native_map)

thinking_level is strict-canonical: it accepts only Goa's six levels (off | minimal | low | medium | high | xhigh). Provider-native values such as max are rejected at validation. When a model expects a native value outside the canonical set — always-thinking or level-restricted models like opencode's x-preview-f-free (which accepts only low | high | max and HTTP-400s on anything else) — remap per model with thinking_level_native_map:

models:
- id: x-preview-f-free
  provider: opencode
  model: x-preview-f-free
  thinking_level: xhigh            # canonical level you pick in the UI
  thinking_level_native_map:       # what actually goes on the wire
    xhigh: max                     # xhigh -> "max" (the value the model accepts)

Each entry maps one canonical level (key) to the provider-native string sent on the wire (value). The merge is per key: a listed level overrides the corresponding entry of the built-in variant profile's map (e.g. kimi-code maps every level to max; listing high: high sends high for that level), while levels you do not list keep the profile's mapping. Profiles without a map (e.g. opencode) get the listed entries verbatim. With no map, the canonical level is sent unchanged.

When you add a custom model through the TUI (/model add or the /config model manager), Goa auto-creates this entry seeded with the model's current effective mapping (or the identity map when the profile defines none), so the knob is visible and you only edit the values the model actually accepts.

Env Var Overrides

Environment variables use the GOA_ prefix with _ as path separator:

# Set active model
export GOA_ACTIVE_MODEL="gpt-4o"

# Override provider endpoint
export GOA_PROVIDERS_0_ENDPOINT="http://192.168.1.100:8080/v1/chat/completions"

# Change execution mode
export GOA_EXECUTION_MODE="confirm"
export GOA_EXECUTION_MAX_TOOL_CALLS="10"

# Enable auto-dreams
export GOA_MEMORY_DREAM_ENABLED="true"
export GOA_MEMORY_DREAM_AUTO="true"
export GOA_TUI_THEME="light"
export GOA_TUI_TRANSPARENCY_SHOW_THINKING="true"
export GOA_TUI_TRANSPARENCY_THINKING_COLLAPSED="false"

# Set API key via env (avoids plaintext in config)
export OPENAI_API_KEY="sk-..."

Env vars support ${VAR} and ${VAR:-default} interpolation in config values.

Stall Timing (provider silence)

execution.activity_timeout is the byte-idle budget of a stream: the window of complete silence — not one single byte — after which goa stops waiting. It is deliberately long, because a reasoning model emits nothing at all while it thinks. A captured z.ai request ran 80.7s of pure silence before its first token; the earlier shipped window of 45s (with a 2-minute fallback) cut healthy reasoning turns short. The shipped default is 2m — still clear of the observed 80.7s thinking window, with the event watchdog retrying dead streams at the 1m30s mark instead of waiting out a longer budget.

That one budget is split between two guards, so they never race on it:

Guard Reset by Deadline Role
Stall warning (execution.activity_warn_after) any visible event 1m20s (derived 2/3 of the event stall when unset) tells the user the provider is quiet, and when the retry runs
Event-stall watchdog mapped stream events only (text/thinking deltas, tool calls, done) 3/4 of the byte budget = 1m30s owns the window: warns, then retries; a round that already delivered a complete answer is finalized instead of replayed
Byte-idle reader any byte, SSE keep-alive comments (: ping) included the full 2m backstop for a socket that delivers nothing at all

So a completely silent stream is terminated by the watchdog (with the warn-then-retry path, and a stream stalled error the retry logic understands), never by the byte reader racing it. A stream that sends keep-alive bytes but no events only the watchdog can catch.

The warning lead must land strictly inside the event stall — a lead between the event stall and the byte budget could never fire. Goa refuses such a pair at load time (per cascade layer) and when set through /config:set; otherwise it derives two thirds of the event stall.

Tuning. Lower the values on a fast local model, raise them on a slow reasoning model:

execution:
  activity_timeout: "2m"        # local 7B on a fast GPU
  activity_warn_after: "45s"    # must be < 1m30s (the 2m window's event stall)

execution:
  activity_timeout: "10m"       # a slow reasoning model with long thinking phases
  activity_warn_after: ""       # let goa derive it (two thirds of the event stall)

A per-provider providers[].idle_timeout overrides the byte budget for that provider only; the same 3/4 split applies. Both keys are editable live at /config → Retry settings or with /config:set execution.activity_timeout=… (the running session picks them up without a restart).

Provider Custom Registry

Goa can fetch provider and model definitions from remote JSON URLs, enabling team-shared configurations:

registry_loaders:
  sources:
    - url: "https://example.com/api.json"
      bearer_token: "${REGISTRY_TOKEN}"   # optional

The registry endpoint should return:

{
  "providers": [
    {"id": "team-openai", "endpoint": "https://proxy.example.com/v1", "api_key": "sk-..."}
  ],
  "models": [
    {"id": "gpt-4o", "provider": "team-openai", "model": "gpt-4o", "max_context_size": 128000}
  ]
}

CLI Flags

All CLI flags override the corresponding config file value:

goa --model gpt-4o                 # Override active model
goa --profile planner              # Override active mode
goa --provider openai              # Override active provider
goa --endpoint http://localhost:1234/v1/chat/completions
goa --api-key sk-...               # Override provider API key
goa --temperature 0.7              # Override model temperature
goa --max-tokens 4096              # Override model max output tokens
goa --max-tool-repeat 5            # Override max identical tool calls in a turn
goa --max-tool-repeat-consecutive 2  # Override max consecutive identical tool calls
goa --max-tool-calls 10            # Override max duplicate calls in the rolling window
goa --tool-call-limit-reset-window 20  # Override rolling-window size
goa --skill-mode inline            # inline | subagent
goa --reasoning                    # Enable model reasoning
goa --thinking-level medium        # off | minimal | low | medium | high | xhigh
goa --thinking-blocks on           # Expand thinking blocks by default
goa --show-thinking                # Show main-agent thinking blocks
goa --theme light                  # dark | light
goa --compression                  # Enable context compression
goa --config ./custom.yaml         # Explicit config path (skips cascade)
goa --debug                        # Enable debug logging

goa --logfile ./goa.log            # Write agent/LLM trace logs to file
# When --logfile is set, --debug is implied and every agent output event is
# traced to the file. Useful for diagnosing hangs or missing responses.

Runtime TUI Toggles

Some TUI settings can be changed without editing config files:

/thinking-blocks                 # Show current thinking-block state
/thinking-blocks:on              # Expand main-agent thinking blocks
/thinking-blocks:off             # Collapse main-agent thinking blocks

/thinking-blocks updates tui.transparency.thinking_collapsed in ~/.goa/config.yaml using a targeted write that preserves your other settings.

Tool output view (Ctrl+O). Press Ctrl+O at runtime to flip every tool block between Summary (a compact N-line preview, the default) and Full (the complete input/output). The toggle applies to all tool blocks for the rest of the session; the starting mode and the Summary line count come from tui.tools.view and tui.tools.preview_lines (see Config File Structure).

Tool Call Display

Each tool call renders as a single widget in the chat that progresses through pending → running → success/error. The widget is created the moment a tool call is detected — even while its arguments are still streaming from the model — so you always see what is happening as fast as possible:

  • Header — the tool name and its key argument (file path, command, …), with a status icon (◉ pending, spinner running, ✓ success, ✗ error).
  • Body — a uniform preview of the call's content: the first N lines of streamed input (file content, code, diff) and the last N lines of output (command stdout, search results). N is tui.tools.preview_lines (default 10).
  • Stats line — a live counter (streaming… 42 lines in · 7 lines out) so long calls stay honest about how much has arrived, plus a Ctrl+O to expand hint when collapsed.

Ctrl+O toggles all tool blocks between Summary and Full for the session. A per-widget expand (focus + Enter) is also available as a secondary affordance.

Configuration

tui:
  tools:
    view: summary        # summary (collapsed, N-line preview) | full (everything)
    preview_lines: 10    # lines per tool block in summary mode

Defaults: view: summary, preview_lines: 10. The preview_lines value is the single source of truth for the collapsed line count across all tools.

First Run Detection

Goa detects whether it has been configured by checking for ~/.goa/config.yaml. If missing, Config.FirstRun is true and the Setup Wizard is launched on startup.

Custom Configuration Files

Beyond the main config cascade, Goa supports custom user override files in ~/.goa/ for specific subsystems:

File Purpose Format
~/.goa/spinner.json Custom spinner animations {"name": {"interval": 100, "frames": ["◜","◠"]}}
~/.goa/search_priority.json Custom file extension priority for search results {"extensions": {".go": 10, ".md": 100}}

Spinner customization: Create ~/.goa/spinner.json with spinner definitions in the same format as the built-in list. All spinners defined in the file are available for selection via /config → Spinner. Example:

{
  "customSpinner": {
    "interval": 120,
    "frames": ["▖", "▘", "▝", "▗"]
  },
  "pulse": {
    "interval": 80,
    "frames": ["█", "▓", "▒", "░", "▒", "▓"]
  }
}

Search priority customization: Create ~/.goa/search_priority.json to override which file types appear first in search results. Lower priority numbers = higher display rank (shown first). The built-in defaults use: source code (10), config files (50), data/doc files (100), media (200). User values are merged on top of the embedded defaults.

{
  "extensions": {
    ".go": 5,
    \“.rs": 5,
    ".txt": 200
  }
}

Hooks

Goa runs lifecycle command hooks at four interception points. Hooks execute with a JSON payload on stdin; a beforeTool hook can veto a tool call by exiting non-zero (goa dialect) or with the Claude Code deny contract (CC dialect). All executions are recorded in <project>/.goa/hooks.log.

Goa-native hooks (hooks.yaml)

Configured in ~/.goa/hooks.yaml (user) and <project>/.goa/hooks.yaml (project; project hooks append after user hooks).

hooks:
  - event: beforeTool
    command: /path/to/guard.sh
    args: []          # optional fixed args prepended to the payload
  - event: afterTool
    command: /path/to/log.sh
  - event: sessionStart
    command: /path/to/start.sh
  - event: sessionEnd
    command: /path/to/end.sh

Events: beforeTool (can veto — non-zero exit), afterTool (never vetoes), sessionStart, sessionEnd. Payloads are goa-shaped (event, tool_name, tool_input, call_id, output, error, session_id, cwd).

Claude Code hooks (hooks.json / settings.json)

An existing Claude Code hook config is accepted as an additional source (gap TL4): ~/.claude/hooks.json, ~/.claude/settings.json (user scope) and <project>/.claude/hooks.json, <project>/.claude/settings.json (project scope). Both the bare event map and the {"hooks": {...}} settings shape are supported; only type: "command" hooks run — http, mcp_tool, prompt, and agent hooks are skipped with a warning.

Supported CC events map onto goa's points: PreToolUse → beforeTool, PostToolUse → afterTool, SessionStart → sessionStart, SessionEnd → sessionEnd. Tool events keep their matcher (tool name; CC literal alternatives match case-insensitively, regex matchers use Go RE2); session matchers are ignored. Hooks receive CC-shaped stdin (hook_event_name, tool_name, tool_input, tool_use_id, tool_response, session_id, cwd), run with sh -c (shell form) or exec form when args is present, in the project directory with CLAUDE_PROJECT_DIR exported and ${CLAUDE_PROJECT_DIR} substituted. The CC default 600s timeout applies.

CC output follows the Claude Code contract: stdout JSON is read on every exit code; exit 2 blocks (JSON cannot override it); otherwise a valid JSON object alone decides via hookSpecificOutput.permissionDecision (allow/deny/ask/defer) or the legacy top-level decision (approve/block). Matching hooks run serially and fold most-restrictively (deny/block > ask/defer > allow); a beforeTool veto error carries every vetoing hook's reason plus the accumulated additionalContext so the model sees it. additionalContext is capped at Claude Code's 10,000-character limit.

Example (denies all Bash calls):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/deny.sh"
          }
        ]
      }
    ]
  }
}

Known limitations (mirroring dsh's bridge): PostToolUse feedback and SessionStart/SessionEnd context are recorded in the audit log but not injected into the conversation; UserPromptSubmit, Stop, SubagentStart, SubagentStop, and the other CC events are not supported; if, async, once, statusMessage, and updatedInput are not honored.

Saving Config Changes

Runtime config changes (e.g., via /config set) are persisted to ~/.goa/config.yaml via ConfigSaver: