Skip to content

Configuration schema ​

The full TOML schema, section by section. Defaults shown when applicable.

Top level ​

toml
default = "homelab"        # optional: name of profile to use when --profile omitted
url = "..."                # if no [profiles.X] tables, top-level fields ARE the default profile
user = "..."
auth = "token" | "password"
token_id = "..."
token_secret = "..."
verify_tls = true

If both default and [profiles.X] are present, default selects which profile to load. If only top-level fields are present, those fields are the implicit profile.

Where the sections below go

The sections on this page ([ssh], [telegram], [[policies]], [pbs], [[alerts]], ...) are shown at the top level, which is where they go in a flat config. In a config made of [profiles.<name>] tables (no top-level url), write them per profile ([profiles.<name>.telegram], [[profiles.<name>.alerts]]) or once for every profile under [defaults] ([defaults.telegram], [[defaults.alerts]]). There the top level may hold only default, profiles and defaults; any other key is a configuration error (exit 3), because no profile would read it.

[defaults] (shared profile base) ​

Optional top-level table merged under every [profiles.<name>] entry before deserialization (#331). Accepts any non-secret, non-url field or sub-table supported by [profiles.<name>]:

toml
default = "pve1"

[defaults]
user          = "root@pam"
auth          = "token"
token_id      = "proxxx"
verify_tls    = true
tls_pin_mode  = "tofu"
rate_limit    = 10
read_only     = false

[defaults.ssh]
user     = "root"
key_path = "~/.ssh/proxxx_homelab"

[profiles.pve1]
url = "https://pve1.lan:8006/"

[profiles.pve2]
url = "https://pve2.lan:8006/"
read_only = true

Merge and validation rules:

RuleBehaviour
PrecedenceA key set in [profiles.<name>] always wins over [defaults].
Sub-tables ([ssh], [pbs], [telegram], [reconcile])Merged one level deep: missing keys in [profiles.<name>.<sub>] are filled from [defaults.<sub>]. Nested maps inside a sub-table (ssh.hosts, ssh.guests) are taken whole from the profile if present, else whole from [defaults] (no recursive map merge).
Arrays ([[policies]], [[alerts]])Not concatenated: if a profile defines policies or alerts, its array replaces the [defaults] array wholesale; if omitted, the profile inherits the [defaults] array.
Forbidden keys (exit 3)url and all secret keys (token_secret, token_secret_file, password, mcp_token, pbs.token_secret, pbs.token_secret_file, telegram.bot_token, telegram.bot_token_file) are refused in [defaults] with ConfigError (exit code 3).
Unknown keys (exit 3)Validated with deny_unknown_fields before merge; any unknown key in [defaults] or its sub-tables fails with a ConfigError naming [defaults].
Flat config (no [profiles])[defaults] is schema-validated, then stripped with a WARN log because no [profiles.<name>] entry is active.

[profiles.<name>] ​

toml
[profiles.homelab]
url           = "https://pve1.lan:8006/"
user          = "root@pam"
auth          = "token"               # token | password
token_id      = "proxxx"
token_secret  = "..."                 # plain string OR
token_secret_file = "/etc/proxxx/token"
password      = "..."                 # only if auth = "password"
verify_tls    = true                  # validate the cluster cert. DEFAULT true
                                      # since v0.13.4 (was false). Proxmox ships
                                      # a self-signed cert: set false deliberately
                                      # for a homelab, or prefer tls_pin_mode.
rate_limit    = 10                    # max API requests/second (default 10).
                                      # ALSO governs TUI refresh latency: a
                                      # refresh costs ~3 requests per node, so
                                      # on an N-node cluster a cycle takes
                                      # roughly 3N/rate_limit seconds. The TUI
                                      # targets a 5 s refresh and warns on
                                      # screen when it cannot keep up (#277).
                                      # 3 nodes ≈ 1 s; 20 nodes ≈ 6 s;
                                      # 50 nodes ≈ 15 s at the default.
read_only     = false                 # true → refuse all mutations on this
                                      # profile client-side (reads still work);
                                      # exit code 8. Default false. Pair with a
                                      # PVEAuditor PVE token for server-side lock.
mcp_token     = "..."                 # bearer token for `proxxx mcp serve-http`.
                                      # Optional; an empty/whitespace value counts as
                                      # ABSENT. serve-http binds loopback by default; on
                                      # a non-loopback bind (e.g. 0.0.0.0) it REFUSES to
                                      # start unless this is set — override consciously
                                      # with --insecure-bind, in which case a network-
                                      # exposed server with no token denies every request
                                      # (fail-closed, and this survives a SIGHUP that
                                      # clears the token). The --token CLI flag overrides
                                      # this field.

[profiles.<name>.reconcile] (GitOps controller) ​

Opt-in continuous reconciliation. When present, proxxx daemon serve runs the drift-watch pillar for this profile; absent → no watch. The one-shot reconcile run / reconcile converge commands take --source/--path on the CLI instead and don't read this block.

toml
[profiles.homelab.reconcile]
source        = "git@github.com:me/cluster.git"  # file, dir, or git URL (shallow-cloned each tick)
path          = "state.toml"          # state file within a dir/git source (default "state.toml")
interval_secs = 300                   # drift-watch poll interval, floored at 30 (default 300)

# ── Layer 3: auto-converge (unmanned mutation) — default OFF ──
auto_converge  = false                # true → after detecting drift each tick, apply it.
                                      # ALWAYS force=false: a Severe-risk drift is NEVER
                                      # auto-applied — it alerts for human review and
                                      # mutates nothing. Respects read_only + incident
                                      # freeze (skips quietly, no alert storm). Disable
                                      # per-process with the `--no-converge` flag or the
                                      # PROXXX_NO_CONVERGE env var.
converge_prune = false                # true → auto-converge MAY execute deletes (maps to
                                      # `state apply --prune`) — but ONLY when
                                      # `max_unmanned_changes` is also set. converge_prune=true
                                      # on its own HOLDS every unmanned delete: creates/updates
                                      # converge while deletes stay previewed-but-held. Default
                                      # false: deletes are previewed and held. Enable only
                                      # against a repo with branch protection / atomic pushes.

# ── Unmanned-converge guardrails (both narrow the blast radius; default = none) ──
allowed_families = ["pool", "acl"]    # restrict the UNMANNED converge to these state
                                      # families (matched against a change's resource:
                                      # pool, acl, storage, backup-job, firewall-*, ha-*,
                                      # notification-matcher, mappings-pci/usb, …). Absent
                                      # or empty = every family (current behaviour). Lets
                                      # you auto-converge low-stakes families while keeping
                                      # high-stakes ones human-only. The manual
                                      # `reconcile converge` command is NOT restricted.
max_unmanned_changes = 5              # hard cap on changes-per-tick for the UNMANNED path,
                                      # counted AFTER allowed_families filtering, regardless
                                      # of severity. Absent = no cap. Above it → the daemon
                                      # refuses and alerts "needs human review (too many
                                      # changes)". Catches a Warning-tier flood (e.g. a
                                      # partial git revert) that the Severe bulk-change
                                      # circuit-breaker (≥50) would miss. ALSO the gate that
                                      # unlocks unmanned prune: converge_prune only deletes
                                      # while this cap is set. Size it SMALL (single digits) —
                                      # a large cap (e.g. 49) re-opens the 10–49
                                      # unmanned-delete band.

[ssh] ​

toml
[ssh]
key_path = "/home/fab/.ssh/proxxx_homelab" # ed25519 / rsa private key path
user = "root"                              # optional, default "root"
strict_host_key_checking = "tofu"           # tofu, strict, off (OpenSSH: accept-new, yes, no); anything else is refused
known_hosts = "~/.config/proxxx/known_hosts"  # optional, default to XDG path

[ssh.hosts]
pve1 = "10.0.0.1:22"                        # optional per-node override; the API-answering node falls back to profile url host:22 (with hostname verification), and other cluster nodes fall back to <node>:22 (required when <node> does not resolve via DNS or /etc/hosts)

[ssh.guests.<vmid>] (optional override) ​

proxxx ssh <vmid> resolves a guest's connection details in two steps: it consults this section first, and on miss auto-discovers via QGA (network-get-interfaces for QEMU) or /lxc/{vmid}/interfaces (for LXC). Most operators don't need to populate this block at all.

Pin a per-guest entry only when:

  • the guest has no qemu-guest-agent installed (or the agent is off / not running),
  • QGA returns only loopback (127.0.0.0/8) or link-local (169.254.0.0/16) IPs — i.e. the guest is on a private bridge with no usable address from your machine's perspective,
  • you want a stable DNS name (web1.lab.example) instead of the rotating DHCP IP QGA would surface.
toml
[ssh.guests."100"]
host = "10.10.10.100"
port = 22
user = "fab"
key_path = "~/.ssh/k8s_master"   # optional, falls back to [ssh].key_path

VMIDs must be quoted (TOML key restriction). The wizard's proxxx init --interactive step 4 sub-prompt builds this section interactively; you can also hand-edit it later.

[telegram] ​

Used by HITL and alert routing.

toml
[telegram]
bot_token = "123456:ABC..."           # from @BotFather
chat_id   = "-1001234567890"            # from getUpdates response
allowed_approvers = [123456789]       # REQUIRED. Telegram numeric user ids
                                      # permitted to approve/deny. The callback
                                      # HMAC proves proxxx minted the keyboard,
                                      # not who pressed it — without this list
                                      # any member of chat_id could approve a
                                      # destructive op. Numeric ids only
                                      # (usernames are mutable). Absent or
                                      # empty => every callback is refused.

[[policies]] (HITL) ​

toml
[[policies]]
action  = "delete"     # delete | stop | restart | migrate | exec | move_disk | resize_disk | *
target  = "tag:prod"   # tag:<X> | <vmid> | *
channel = "telegram"   # approval channel
require = 1            # approvals needed (2 or more = quorum)

Multiple [[policies]] arrays are evaluated in order. The first matching one wins.

[pbs] ​

For Proxmox Backup Server browse and restore.

toml
[pbs]
url                = "https://pbs.lan:8007/"
user               = "proxxx@pbs"
token_id           = "reader"
token_secret       = "..."
token_secret_file  = "/etc/proxxx/pbs-token"
verify_tls         = false
rate_limit         = 10

WARNING

PBS uses : between token_id and secret in the auth header, not = like PVE. proxxx handles this internally — you don't need to pre-format the secret.

[[alerts]] ​

toml
[[alerts]]
name              = "node_down"
trigger           = "node_offline"           # closed enum: see Alerts integration
for_secs          = 60                        # seconds
storage           = "ceph-rbd"                # filter for storage_above (optional)
threshold_percent = 85                        # for storage_above
severity          = "critical"                # info | warning | critical
route             = ["telegram", "ntfy:topic"]
dedup_secs        = 600                       # don't re-fire within N seconds

The trigger is a closed enum (node_offline, storage_above, replication_failing). New triggers require code changes — that is intentional, see Alerts.

Resolution order for secrets ​

For each of token_secret, password, pbs.token_secret:

  1. CLI flag (--token-secret VALUE, --pbs-token-secret VALUE)
  2. Env var (PROXXX_TOKEN_SECRET, PROXXX_PASSWORD, PROXXX_PBS_TOKEN_SECRET, PROXXX_TELEGRAM_BOT_TOKEN)
  3. Inline TOML value (<...>_secret = "...")
  4. File reference (<...>_secret_file = "...")
  5. macOS Keychain (macOS only; service proxxx, account <profile>/<item> then <item> for token_secret, password, pbs_token_secret, and telegram_bot_token)

The first one that resolves wins.

Inline beats the file reference

Steps 3 and 4 were documented in the opposite order until v0.13.4. If you are moving a secret out of the TOML into a 0600 file, delete the inline value — otherwise the stale inline secret keeps winning, your file is never read, and rotating it has no effect. proxxx logs a warning when both are set.

Loaded values live in SecretString: Debug prints [REDACTED] (not even the length, which would leak which credential class it is), there is no Display and no Serialize, so interpolating one into a string or a JSON dump is a compile error, and the wrapped value is zeroized on drop. Note the guarantee covers the value once constructed — the toml parse tree still holds an unwiped copy of any inline secret until config load completes, which is another reason to prefer the file or the keychain.

Environment variables ​

Beyond the secret env vars above and PROXXX_CONFIG (see File location), these tune runtime behaviour:

VariableEffect
PROXXX_NO_CONVERGEDisable auto-converge for this process (same as --no-converge): the drift-watch still detects and alerts, but mutates nothing.
PROXXX_AUDIT_DIRRelocate the audit DB and its HMAC key off the default path (e.g. onto a separate volume). The directory is created 0700.
PROXXX_LOG_DIRRelocate the rotating log files (proxxx.<date>.log, 14 kept; default: the platform data dir, the same directory as the audit DB). A directory proxxx creates is 0700; the default data dir is narrowed to 0700 on start, while an existing PROXXX_LOG_DIR keeps its mode, so keep it owner-only (log files take the process umask).
PROXXX_MCP_TOKENBearer token for mcp serve-http, kept out of the process listing. --token-file and --token win over it; it wins over mcp_token in the profile.
PROXXX_CACHE_DIRRelocate the TUI / HITL state cache (<profile>_state.db, default <platform cache dir>/proxxx). It is only a cache: deleting it loses nothing that is not re-fetched.
PROXXX_AUDIT_KEYRelocate only the HMAC key, off the DB volume. A group/world-readable audit.key is refused on load (unix); proxxx audit verify exits non-zero on tamper.

Defaults summary ​

FieldDefault
verify_tlstrue
rate_limit10
user (SSH)"root"
dedup_secs (alert)600
severity (alert)"warning"
auth"token"

File location ​

PlatformPath
Linux~/.config/proxxx/config.toml
macOS~/Library/Application Support/dev.proxxx.proxxx/config.toml
Windows%APPDATA%\dev\proxxx\proxxx\config.toml

Set PROXXX_CONFIG=/path/to/file.toml to override.

Validation ​

proxxx validates the schema on load. A failed validation prints the section and key, then exits with code 3 (Configuration error). Common errors:

  • url missing trailing colon — proxxx adds /api2/json itself, so the URL stops at :8006/
  • token_secret has trailing whitespace — copy-paste from the web UI sometimes appends a newline; trim it
  • auth = "token" without token_id — both fields are required
  • A top-level section in a profile-only config — with [profiles.<name>] tables and no top-level url, a top-level [[alerts]], [telegram], [ssh] or misspelled key is refused, naming the key; move it under [defaults] or [profiles.<name>]
  • [ssh] referenced but key_path is missing — proxxx ssh <vmid>, proxxx perms, and proxxx patch apply all require it

Released under the MIT License. · Privacy & legal