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 aCtrl+O to expandhint 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: