Configuration Guide

September 8, 2026 · View on GitHub

All configuration is stored in ~/.servonaut/config.json. The file is created automatically on first run with sensible defaults.

Configuration Reference

{
  "version": 2,
  "default_key": "/home/user/.ssh/my-default-key.pem",
  "instance_keys": {
    "i-0123456789abcdef0": "/home/user/.ssh/special-key.pem"
  },
  "default_username": "ec2-user",
  "cache_ttl_seconds": 3600,
  "terminal_emulator": "auto",
  "theme": "dark",
  "keyword_store_path": "~/.servonaut/keywords.json",
  "default_scan_paths": ["~/shared/", "/var/log/app.log"],
  "scan_rules": [],
  "connection_profiles": [],
  "connection_rules": []
}
FieldTypeDefaultDescription
versionint2Config schema version (auto-migrated from v1)
default_keystring""Default SSH key path for all instances
instance_keysobject{}Instance-specific key mappings {instance_id: key_path}
default_usernamestring"ec2-user"Default SSH username
cache_ttl_secondsint3600Instance cache TTL in seconds (1 hour)
terminal_emulatorstring"auto"Terminal preference (see Supported Terminals)
themestring"dark"UI theme: dark or light
keyword_store_pathstring"~/.servonaut/keywords.json"Path to keyword scan results file
default_scan_pathsarray["~/"]Default paths to scan on all instances
scan_rulesarray[]Conditional scan rules (see Scan Rules)
connection_profilesarray[]SSH connection profiles (see Connection Profiles)
connection_rulesarray[]Rules for applying profiles (see Connection Rules)

Match Conditions

Match conditions are used by both scan rules and connection rules to target specific instances. All conditions in a rule are AND-ed together — every condition must match.

ConditionTypeDescription
name_containsstringCase-insensitive substring match on instance name
name_regexstringRegular expression match on instance name (case-insensitive)
regionstringExact region match (e.g., us-east-1)
idstringExact instance ID match
type_containsstringSubstring match on instance type (e.g., t3)
has_public_ipstring"true" or "false" — whether instance has a public IP

Scan Rules

Scan rules define what paths to search and commands to execute when scanning servers. Rules only apply to instances matching their conditions.

{
  "scan_rules": [
    {
      "name": "Web server logs",
      "match_conditions": {
        "name_contains": "web",
        "region": "us-east-1"
      },
      "scan_paths": [
        "/var/log/nginx/access.log",
        "/var/log/nginx/error.log"
      ],
      "scan_commands": [
        "grep -r 'ERROR' /var/www/html/logs/"
      ]
    }
  ]
}
FieldTypeDescription
namestringDescriptive name for the rule
match_conditionsobjectConditions to match instances (see Match Conditions)
scan_pathsarrayFile paths to scan for keywords on matching instances
scan_commandsarrayShell commands to run on matching instances

Scan results are stored persistently in the keyword store and searchable from the TUI.

Connection Profiles

Connection profiles define how to connect to instances, including bastion/jump host configuration.

{
  "connection_profiles": [
    {
      "name": "private-vpc-bastion",
      "bastion_host": "bastion.example.com",
      "bastion_user": "ubuntu",
      "bastion_key": "/home/user/.ssh/bastion-key.pem",
      "username": "ubuntu",
      "ssh_port": 22,
      "extra_ssh_options": []
    }
  ]
}
FieldTypeDefaultDescription
namestringProfile identifier (referenced by connection rules)
bastion_hoststringBastion hostname or IP
bastion_userstring"ec2-user"Username for bastion connection
bastion_keystringSSH key for bastion (optional — if omitted, uses same key as target)
usernamestringSSH username for the target host (overrides default_username)
proxy_commandstringCustom ProxyCommand (optional — overrides bastion settings)
ssh_portint22SSH port on bastion host
extra_ssh_optionsarray[]Extra -o KEY=VALUE entries for the target connection (see Per-host SSH tuning)

How Proxy Works

The proxy method is chosen automatically based on what's configured:

ConfigurationSSH MethodUse Case
bastion_key is set-o ProxyCommand with -i flagBastion needs a different key than the target
No bastion_key-J (ProxyJump)Bastion uses same key or SSH agent
proxy_command is set-o ProxyCommand (raw)Advanced/custom proxy setups

When a bastion profile matches, the target host automatically switches to the instance's private IP.

Per-host SSH tuning

extra_ssh_options lets you pass arbitrary -o KEY=VALUE flags to a specific subset of hosts without weakening your global SSH defaults. Each entry is the KEY=VALUE string — the leading -o is added automatically. The options are applied before proxy/identity flags, so they also flow through bastion connections.

The same field is also available on each custom_servers entry (see Custom Servers), and both are merged together at connect time — profile options first, then custom-server options.

Common uses:

GoalEntry
Talk to a legacy OpenSSH (< 7.2) server that only supports ssh-rsa (SHA-1)"HostKeyAlgorithms=+ssh-rsa,ssh-dss" + "PubkeyAcceptedAlgorithms=+ssh-rsa"
Enable old ciphers on an ancient host"Ciphers=+aes128-cbc"
Keep long SSH sessions alive through a NAT"ServerAliveInterval=30", "ServerAliveCountMax=3"
Bump the connect timeout for flaky networks"ConnectTimeout=20"
Force IPv4"AddressFamily=inet"

Legacy host example (a profile matched via a connection rule):

{
  "connection_profiles": [
    {
      "name": "legacy-shared-hosting",
      "username": "appuser",
      "extra_ssh_options": [
        "HostKeyAlgorithms=+ssh-rsa,ssh-dss",
        "PubkeyAcceptedAlgorithms=+ssh-rsa"
      ]
    }
  ]
}

Security note: Re-enabling SHA-1 signatures (ssh-rsa) or DSA (ssh-dss) weakens the cryptographic guarantees of the connection. Scope these options to the specific hosts that need them via extra_ssh_optionsnever set them globally in your ~/.ssh/config.

Live SSH monitoring

Press L on a server's actions screen to start or stop the compact live metrics section. It shows CPU, memory, load, root disk usage, and uptime alongside the server's other details and actions.

Monitoring runs read-only Linux commands. It needs SSH access with a local key or an available SSH agent; it cannot prompt for a password or unlock a key. If authentication fails, check the SSH username and key, then press L to retry. Polling stops when you leave the screen.

For OVH, monitoring uses ovh.default_username (or the provider's default username) and chooses the key in this order: instance_keys, ovh.default_ssh_key, default_key, then local key discovery. Bitwarden SSH refs used by interactive SSH Connect are not resolved by live monitoring; load the corresponding key into your SSH agent or configure a local key.

The ssh configuration object accepts these monitoring settings:

FieldDefaultDescription
live_stats_interval_seconds3.0Delay after each successful sample; must be positive
live_stats_timeout_seconds20.0Total SSH command deadline, including connection setup; must be positive

Existing configurations receive these defaults automatically. Choose a monitoring timeout long enough for the configured SSH connect_timeout and the remote command to complete.

Custom Servers

Non-AWS servers (DigitalOcean, Hetzner, bare-metal, shared hosting, etc.) live under custom_servers. They show up in the instance list alongside AWS instances and use the same SSH/SCP/log-viewer UI.

{
  "custom_servers": [
    {
      "name": "my-vps",
      "host": "203.0.113.10",
      "username": "root",
      "ssh_key": "~/.ssh/vps-key",
      "port": 22,
      "provider": "Hetzner",
      "group": "web",
      "tags": { "env": "prod" },
      "extra_ssh_options": []
    }
  ]
}
FieldTypeDefaultDescription
namestringUnique server identifier (shown in the instance list)
hoststringHostname or IP address
usernamestring"root"SSH username
ssh_keystring""Path to SSH key file (supports ~ expansion)
portint22SSH port — forwarded to both ssh -p and scp -P
providerstring""Free-form provider label (e.g., "Hetzner")
groupstring""Optional grouping label for match conditions
tagsobject{}Arbitrary key/value metadata, targetable via tag:<key> match conditions
extra_ssh_optionsarray[]Extra -o KEY=VALUE entries (see Per-host SSH tuning)

Custom servers can also be added/edited/removed from the Custom Servers screen in the TUI, including the extra_ssh_options field as a multi-line input.

Connection Rules

Connection rules link profiles to instances via match conditions.

{
  "connection_rules": [
    {
      "name": "Private instances via bastion",
      "match_conditions": {
        "name_contains": "private",
        "region": "us-west-2"
      },
      "profile_name": "private-vpc-bastion"
    }
  ]
}
FieldTypeDescription
namestringRule description
match_conditionsobjectConditions to match instances (see Match Conditions)
profile_namestringName of connection profile to apply

Rules are evaluated in order — the first matching rule wins. If the referenced profile doesn't exist, a warning is shown in the command overlay.

AI Provider

Configure AI log analysis under the ai_provider key. Each provider has its own dedicated API-key field so keys don't leak across providers:

{
  "ai_provider": {
    "provider": "openai",
    "openai_api_key": "$OPENAI_API_KEY",
    "anthropic_api_key": "$ANTHROPIC_API_KEY",
    "gemini_api_key": "$GEMINI_API_KEY",
    "ollama_api_key": "",
    "model": "",
    "base_url": "",
    "max_tokens": 2000,
    "temperature": 0.3
  }
}
FieldTypeDefaultDescription
providerstring"openai"Active provider: openai, anthropic, gemini, ollama, or servonaut
openai_api_keystring""OpenAI key (supports secret references — see below)
anthropic_api_keystring""Anthropic key (supports secret references)
gemini_api_keystring""Google Gemini key (supports secret references)
ollama_api_keystring""Optional Ollama Cloud key — leave empty for local installs
api_keystring""Legacy. Pre-v4 single shared key. Still read on disk for one-release rollback safety; new configs should populate the per-provider fields above
modelstring""Model name (empty = provider default)
base_urlstring""Custom API base URL — set to https://ollama.com to point Ollama at the cloud instead of http://localhost:11434
max_tokensint2000Maximum response tokens
temperaturefloat0.3Sampling temperature

Default models per provider: OpenAI → gpt-4o-mini, Anthropic → claude-sonnet-4-20250514, Gemini → gemini-2.0-flash, Ollama → llama3. When using Ollama Cloud, model names take no -cloud suffix (e.g. gpt-oss:120b); the suffix is only used by local Ollama proxying to a cloud model.

No extra install needed — httpx ships as a base dependency.

Secrets

API keys and other sensitive values can be externalized so config.json is safe to commit to a dotfiles repo.

Secret Reference Syntax

Any config value that accepts secrets supports three formats. Fields treated as secrets today: ai_provider.openai_api_key, ai_provider.anthropic_api_key, ai_provider.gemini_api_key, ai_provider.ollama_api_key, the legacy ai_provider.api_key, and abuseipdb_api_key.

FormatExampleHow it resolves
$ENV_VAR$OPENAI_API_KEYReads from environment variable
file:pathfile:~/.secrets/openai_keyReads file contents (whitespace-stripped)
Plain textsk-abc123...Used as-is

Auto-loading Secrets File

If ~/.secrets/servonaut.env exists, it is loaded automatically on startup. This file uses simple KEY=value syntax:

# ~/.secrets/servonaut.env
OPENAI_API_KEY=sk-abc123...
ANTHROPIC_API_KEY=ant-abc123...

Rules:

  • Existing environment variables are not overwritten (env takes precedence)
  • # comments and blank lines are supported
  • Values may be optionally quoted with single or double quotes
  • The file is silently skipped if it doesn't exist

Example Setup

~/.servonaut/config.json (safe to commit):

{
  "ai_provider": {
    "provider": "openai",
    "openai_api_key": "$OPENAI_API_KEY",
    "anthropic_api_key": "$ANTHROPIC_API_KEY"
  }
}

~/.secrets/servonaut.env (gitignored, stays local):

OPENAI_API_KEY=sk-LwhfdskfjdhskfwueihfFJ...
ANTHROPIC_API_KEY=sk-ant-...

Or using a file reference instead:

{
  "ai_provider": {
    "openai_api_key": "file:~/.secrets/openai_key"
  }
}

Environment Variables

These environment variables override hardcoded API endpoints. Useful for pointing the CLI at a staging server.

VariableDefaultDescription
SERVONAUT_API_URLhttps://api.servonaut.devBase URL for the Servonaut API (auth, config sync, teams, entitlements)
SERVONAUT_MCP_URLhttps://mcp.servonaut.devBase URL for the hosted MCP server (premium tools)
SERVONAUT_RELAY_TOKENLegacy/CI override: auth token for servonaut connect (the stored servonaut login session is used when unset)
SERVONAUT_USER_IDLegacy/CI override: user ID for servonaut connect

These can be set inline, exported, or added to ~/.secrets/servonaut.env:

# Point CLI at staging
SERVONAUT_API_URL=https://staging.example.com
SERVONAUT_MCP_URL=https://staging.example.com

Relay Listener (relay)

Settings for the Mercure SSE relay used by servonaut connect and the TUI's in-process listener:

{
  "relay": {
    "base_url": "https://api.servonaut.dev",
    "mercure_url": "https://servonaut.dev/.well-known/mercure",
    "heartbeat_interval": 30,
    "ai_tool_auto_approve": "standard"
  }
}
KeyDefaultDescription
base_url(derived from API base)REST API for heartbeats, Mercure JWTs, and results
mercure_url(derived from API base)The Mercure hub URL
heartbeat_interval30Seconds between heartbeats
ai_tool_auto_approve"standard"Max guard tier a headless listener auto-approves for AI chat tool calls: "readonly", "standard", or "dangerous". "dangerous" additionally requires the dangerous-AI-tools entitlement. Tools above the tier are denied with an explanatory message.

Supported Terminals

Set terminal_emulator to one of the following, or "auto" for automatic detection:

  • gnome-terminal
  • konsole
  • alacritty
  • kitty
  • xterm
  • xfce4-terminal
  • mate-terminal
  • tilix
  • Terminal.app (macOS)
  • iTerm.app (macOS)
  • wt.exe (Windows Terminal)

Servonaut AI

Servonaut AI is a hosted AI gateway included with Solo and Teams plans on servonaut.dev. It requires no local API key — authentication is handled by your existing Servonaut Cloud session (TUI → Account → Login). Once subscribed, the provider is active automatically: open the AI chat panel in the TUI, or run servonaut ai chat from the command line, and your prompts are routed through the gateway. The hosted model can tail logs, run commands, and triage incidents on your servers through the existing Mercure relay — your AWS credentials never leave the CLI.

Enabling Servonaut AI

Sign in from the TUI: launch servonaut, open Account → Login in the sidebar, and approve the device-flow prompt at servonaut.dev.

After login the CLI fetches your entitlements. If your plan includes premium_ai, the Servonaut AI provider becomes available in the provider picker (TUI Settings panel or --ai-provider servonaut). No further configuration is needed.

Provider settings

Servonaut AI adds three fields to the ai_provider config block:

{
  "ai_provider": {
    "provider": "openai",
    "provider_preference": "servonaut",
    "local_fallback_provider": null,
    "dismissed_banners": []
  }
}
FieldTypeDefaultDescription
providerstring"openai"Active provider for the current config (mutated by Settings)
provider_preferencestring|nullnullPersistent preference set by the first-run picker or servonaut ai provider reset. When set, overrides the decision tree at every chat-start.
local_fallback_providerstring|nullnullOpt-in local fallback on repeated upstream_unavailable errors. Accepts "ollama", "openai", or "anthropic". Default null means no automatic fallback — you will be offered a one-shot per-session prompt instead. Ollama is the recommended value for privacy (prompts stay on-machine).
dismissed_bannersarray[]IDs of banners the user has dismissed forever. Managed automatically; cleared by servonaut ai provider reset.

How the provider picker decides:

PlanOther providers configuredExplicit preferenceCLI does
Solo/TeamsYesYesHonour preference
Solo/TeamsYesNoShow first-run modal; persist choice
Solo/TeamsNon/aUse Servonaut AI (only option)
FreeYesYesHonour preference
FreeYesNoUse first-configured provider
FreeNon/aEmpty-state onboarding

Run servonaut ai provider reset to clear provider_preference and dismissed_banners and trigger the picker again on next chat start.

allow_dangerous_ai_tools

This entitlement is set by a Teams plan administrator and is opt-in — it is false by default for all accounts. When false, the tools deploy, provision, and security_scan are hidden from the chat panel and any server-side call for those tools will be rejected (the server enforces this independently). When true, those tools appear in the panel and require a typed confirmation ("type RUN to confirm") before execution. The setting is cached locally from /api/entitlements and refreshed on each login and chat response; mid-session changes take effect on the next refresh_entitlements() cycle, not mid-stream.

Top-up flow

When your monthly token quota is exhausted you will see a modal with a Top up button. Clicking it (or running servonaut ai topup [pack]) calls POST /api/ai/topup/checkout and opens the resulting Stripe Checkout URL in your default browser. The CLI does not embed Stripe. After completing the purchase, your tokens_topup_remaining balance typically refreshes within 60 seconds (the CLI schedules two background entitlement fetches at +30 s and +60 s to absorb webhook latency).

Top-up packs: small, medium, large (canonical names; pricing at servonaut.dev/account/billing/topup).

Error codes

CodeWhat it meansCLI response
`rate_limited$\text{You} \text{are} \text{sending} \text{requests} \text{too} \text{fast}\text{Auto}-\text{retries} \text{up} \text{to} 3 \times \text{with} $retry_after` + jitter; toast if all retries fail
quota_exhaustedMonthly token allowance is used upTop-up modal with link to billing; no auto-retry
budget_exhaustedYour per-period cost cap has been reachedSame modal; shows $X.XX of $Y.YY used
free_not_entitledThis path requires Solo or TeamsUpgrade modal linking to /pricing
entitlement_requiredpremium_ai is false for your accountSame upgrade modal; triggers refresh_entitlements() first in case of stale cache
service_unavailableServonaut AI feature flag is offBanner: "AI temporarily off"; offers fallback to your local provider if configured
upstream_unavailableAll vendor backends exhaustedSame banner; if fallback_used was already true, adds "all vendors flaky" note
context_too_largeMessage history exceeds ~200 k tokensCLI auto-chunks via chunk_text and retries once
content_blockedSafety filter rejected the responseToast "Response blocked by safety filter"; raw payload is logged but never displayed
validation_failedMalformed request bodyToast "Internal error — please report"; details written to debug log only

Exit codes for servonaut ai * commands

CodeMeaning
0Success
1Other / unknown error
2Unauthenticated — run servonaut login
3Insufficient entitlement — requires Solo or Teams plan
4Quota exhausted — run servonaut ai topup
5Budget exhausted — cost cap reached; run servonaut ai topup

Config Migration

If you're upgrading from v1 (flat configuration structure), the app automatically migrates to v2 on first load. The v1 bastion settings are converted to a connection profile and rule. No manual action required.

Runtime Files

All runtime files are stored under ~/.servonaut/:

FilePurpose
~/.servonaut/config.jsonMain configuration
~/.servonaut/cache.jsonCached instance list with timestamp
~/.servonaut/keywords.jsonKeyword scan results
~/.servonaut/command_history.jsonSaved commands and command history
~/.servonaut/logs/servonaut.logApplication log
~/.servonaut/logs/servonaut_*.shTemporary SSH wrapper scripts