Skip to content

Configuration

Daemon Configuration

The daemon reads its config from a TOML file at the platform-specific config directory. On first run, a default config with an auto-generated auth token is created.

Config File Location

Determined by the directories crate using ProjectDirs::from("com", "hive", "Hive"):

PlatformPath
Linux~/.config/hive/Hive/config.toml
macOS~/Library/Application Support/com.hive.Hive/config.toml
WindowsC:\Users\<user>\AppData\Roaming\hive\Hive\config\config.toml

The config file is automatically created by hive install, which runs an interactive wizard to set these values. You can also create or edit the file manually.

When the daemon is launched with --config <path>, Hive anchors related paths to that config location. In the default layout this means:

  • ...\config\config.toml
  • ...\data\ for the SQLite DB, logs, and pasted-file attachments

This is especially relevant for the Windows service install: the service runs as LocalSystem, but Hive pins --config to the installing user's roaming profile so attachments and other daemon data stay under that same user profile instead of C:\Windows\System32\config\systemprofile\....

Inspect the Clusto Layout Preparation

The current rename preparation is layout epoch 1. It includes the complete, journaled migration and recovery engine, but epoch 1 never starts a cutover automatically. Hive paths and the hived service remain authoritative until the separately released epoch-2 image performs the handoff.

On A0 startup, hived prepares a durable rollback set under the reported pre-clusto-backup directory. It contains the running A0 daemon, the matching CLI, the exact old service definition, the resolved path map, and SHA-256 checksums. Journal generations are append-only and hash-linked. Startup treats preparation as best-effort so development launches still work, but a manifest URL self-update is blocked unless the backup is complete and verifies. An existing prepared backup is validated and reused, never overwritten.

Operators can inspect the resolved paths and prerequisites without stopping a service or changing either layout:

bash
hived migrate-layout --dry-run

The report includes:

  • layout_epoch and whether automatic cutover is enabled
  • the old and new config, data, attachment, daemon, CLI, and service paths
  • every old/new occupancy and collision result
  • layout disposition, such as a legacy install, fresh Clusto install, completed migration, empty layout, or collision
  • service-manager support and detached-helper availability
  • rollback_backup_ready and the combined ready_for_cutover result

It is read-only. Running it does not create the backup, move files, install services, or launch a helper.

Use JSON when feeding the same inspection into automation:

bash
hived migrate-layout --dry-run --json

If the daemon service uses an explicit config file, pass that exact path so the report resolves its related data paths correctly:

bash
hived migrate-layout --dry-run --config /etc/hive/config.toml

During epoch 1, runtime environment lookups prefer a CLUSTO_* value when both names are set and fall back to the corresponding HIVE_* value. Child agent processes receive both names so either prefix works during the preparation period.

The overlap also registers and browses both _hive._tcp.local. and _clusto._tcp.local.. Discovery merges the two announcements by stable node ID and tries alternate advertised addresses before reporting a node unreachable.

The epoch-2 handoff runs outside the service being replaced. Linux launches a transient clusto-layout-migration systemd unit. Windows creates and starts the ClustoLayoutMigration scheduled task as SYSTEM, with boot-time retry if the first attempt is interrupted. The helper verifies the A0 backup and staged A1 daemon and CLI, journals each operation, stops the old service, moves state, starts clustod, and probes its expected version and node name. A failed step or health probe enters the same rollback path.

Roll Back a Layout Migration

The rollback command is safe to run during A0 preparation:

bash
hived migrate-layout --rollback

With no journal it reports that there is nothing to do. With only a prepared A0 backup it verifies the rollback artifacts and leaves the live Hive layout unchanged. After a handoff has started, run the command from the available hived or clustod binary. It stops the new service, moves current Clusto state back to the recorded Hive paths, restores the checksummed A0 daemon, CLI, and service definition, starts hived, probes it, and writes a durable rolled-back marker.

Rollback refuses to replace unexpected data or executables already occupying an old path. Inspect that data first. If it must be preserved while recovery continues, use the explicit recovery option:

bash
clustod migrate-layout --rollback --force-recovery

--force-recovery moves each conflicting old-side item into the migration journal's recovery-conflicts/<migration-id>/ directory before restoring the recorded layout. It does not silently delete the conflict. Do not create or populate the reported Clusto destinations by hand; unexplained occupancy is a hard cutover gate.

Full Config Reference

toml
# Network binding address. Use "0.0.0.0" for all interfaces.
bind = "127.0.0.1"

# Client WebSocket port.
port = 9178

# Auth token for client connections. Auto-generated if omitted.
token = "a3f8b2c1d4e5..."

# Path to the Claude CLI binary. Resolved via PATH if just "claude".
claude_bin = "claude"

# Human-readable name for this node. Shown in cluster status.
# display_name = "my-node"

# Default working directory when client sends "." as working dir.
# default_working_dir = "/home/user/projects"

# Extra arguments appended to every Claude session this node spawns (both
# interactive PTY sessions and headless SDK sessions). Alias: claude_extra_args.
# Arguments already present from the command registry or the caller are not
# duplicated.
# arguments = ["--dangerously-skip-permissions"]

# Default OS user new sessions run as when neither an explicit run_as_user nor a
# project-level user mapping is provided. Empty / absent runs sessions as the
# daemon process user. Prefer a non-root account: an agent reads the credentials
# and settings of the account it runs as, and the Claude CLI refuses
# --dangerously-skip-permissions under root, so root-spawned bypass-mode team
# members cannot start at all.
# default_session_user = "hive"

# Directory roots pre-trusted for Claude PTY sessions (Unix only). Before a
# claude session spawns with a working dir under one of these roots, the daemon
# marks that folder trusted in the spawn user's ~/.claude.json so headless
# `hive send` into a fresh session is not swallowed by Claude's "trust this
# folder" dialog. Respects run_as_user / default_session_user (it writes that
# user's ~/.claude.json, chowned to them).
# trusted_dirs = ["/home/hive/projects", "/srv/work"]

# When true, PTY sessions are launched inside a named tmux session so they
# survive a daemon restart. On restart the daemon re-adopts any surviving
# hive-<uuid> tmux sessions instead of treating them as dead.
# Requires tmux installed on this node. Default: false.
# persistent_sessions = true

# Tracing-filter directive for the daemon's log subscriber.
# Default is "warn" (errors and warnings only). Set to "info" or "debug" while
# reproducing an issue, then revert. Accepts the same syntax as RUST_LOG, e.g.
# "hived=debug,hyper=warn". The RUST_LOG env var, when set, takes precedence
# over this field.
# log_filter = "info"

# Optional: agent-idle detection tuning.
# The daemon watches each PTY session's terminal title to detect when an
# interactive agent CLI stops working. This drives the in-app desktop
# popup/sound and the durable session name; there are no remote push channels.
# Omit this section to use the built-in Claude/Codex rules.
# [[notifications.agent_detection]]
# name = "claude"
# working_title_regex = '^\s*[\x{25D0}\x{25D1}]'   # Claude circle spinner

# Optional: cluster configuration for multi-node mode.
# Omit this entire section for standalone mode.
[cluster]
# Auto-generated node UUID. Do not set manually.
# node_id = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"

# Peer node addresses in host:peer_port format.
peers = ["10.0.0.2:9179", "10.0.0.3:9179"]

# Port for inter-node peer WebSocket communication.
peer_port = 9179

# Shared secret for peer authentication. Must be identical on all nodes.
cluster_token = "shared-cluster-secret"

# Deprecated, no-op. Leaderless clustering has no leader, election, or quorum.
# These keys are still parsed for backward compatibility but no longer affect
# any behaviour; new configs can omit them.
heartbeat_interval_ms = 1000
election_timeout_min_ms = 3000
election_timeout_max_ms = 5000
# min_quorum = 2

# This node's client-facing addresses advertised to peers and clients.
# The first entry is treated as primary.
# If omitted, Hive auto-detects an address from the local machine.
# advertise_addresses = ["10.0.0.1:9178"]

# Human-readable name for this node in cluster context.
# Overrides top-level display_name when set.
# display_name = "node-a"

# Optional: Hive Tunnel (premium) - Tailscale-free reachability for this node.
# Off by default; normally toggled from Settings > Cluster in the app, not by
# hand. Changes take effect after a daemon restart. See docs/clusto-tunnel.md.
# [tunnel]
# enabled = true
# Auto-generated base64 ed25519 secret key; its public half is the node's
# tunnel endpoint id shown in the app. Do not set manually.
# secret_key = "..."
# Optional relay override; defaults to the Hive-hosted relay.
# relay_url = "https://relay.vazac.dev:8443"

[notes]
# Markdown notes vault, S3-backed with a per-node local cache. See docs/notes.md.
enabled = true
# S3-compatible bucket (sync is disabled without it; the vault stays local-only).
bucket = "my-notes"
# Endpoint for non-AWS providers (MinIO/R2/B2). Omit for AWS S3.
# endpoint = "https://s3.amazonaws.com"
region = "us-east-1"
# Credentials. Omit to fall back to AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY.
# access_key = "AKIA..."
# secret_key = "..."
# Local vault directory. `~` expands to home; defaults to <data_dir>/notes.
local_dir = "~/.hive/notes"
# Background pull cadence in seconds (minimum 60).
sync_interval_secs = 300

Full-text search runs natively inside the daemon - no external tools required. Notes chats choose their provider and model through the shared New chat dialog. See Notes for the complete feature reference.

OpenCode

OpenCode is a CLI provider. Install it on every node that runs OpenCode agents:

bash
npm install -g opencode-ai

Authenticate as the same OS account Hive resolves for the chat or team. For a normal OpenCode provider, run the login command and select the upstream provider you want OpenCode to use:

bash
opencode auth login

The login is stored for that OS account. If hived runs agents through default_session_user or a Project's Run As User, logging in as your shell account or as root does not configure that agent account.

Enter OpenCode models in provider/model form.

Override the binary or append node-wide arguments with the normal agent table:

toml
[agents.opencode]
bin = "/opt/opencode/bin/opencode"

Native OpenRouter API

OpenRouter is independent of OpenCode. Hive calls the native OpenRouter HTTP API directly, streams its response, maintains conversation history, and runs workspace tools in the daemon. Configure the node with either environment:

bash
OPENROUTER_API_KEY=sk-or-v1-...

or private config.toml settings:

toml
[openrouter]
api_key = "sk-or-v1-..."

You can manage the private config value from the connected app instead of editing TOML. Open Settings > Appearance > OpenRouter API, or press F1 and run Configure OpenRouter API key. The card reports whether the connected node has an effective key and whether it comes from private config or the daemon environment. The key itself is write-only and is never sent back to the app.

Enter a new key to save or replace [openrouter].api_key. Clear stored key removes only that private config value. If the daemon has an OPENROUTER_API_KEY, clearing the stored value makes the environment key active again; environment-provided keys cannot be cleared in the app and must be changed in the service environment. Saved changes apply to the next OpenRouter turn without restarting hived.

OpenRouter uses native model ids such as anthropic/claude-sonnet-4-5. With no explicit model Hive sends openrouter/auto. The API key belongs to the node, not the chat's Run As user. Run As still controls filesystem and command tools. No OpenCode binary, OpenCode login, or [agents.openrouter] entry is used for OpenRouter sessions.

OpenCode models are probed with opencode models. OpenRouter models come from the public OpenRouter catalog. You can add or override picker entries in the same model catalog used by other providers:

toml
[[models.extra.opencode]]
id = "anthropic/claude-sonnet-4-5"
tier = "balanced"

[[models.extra.openrouter]]
id = "anthropic/claude-sonnet-4-5"
tier = "balanced"

See Chats and Teams for provider-specific runtime behavior.

Model catalog ([models])

Controls the model list offered when picking an agent for a team member (see Teams). Optional - the defaults work with no configuration.

toml
[models]
# How long a fetched per-provider model list is cached. Default: 6.
refresh_hours = 6
# Consult the keyless OpenRouter public index for ids released after this
# build. Set false on nodes that must not reach the internet. Default: true.
openrouter = true

# Extra or overriding entries, keyed by provider id. An entry whose `id`
# already exists overrides that model's label and tier.
[[models.extra.claude]]
id = "claude-opus-5"
label = "House deep model"
tier = "deep"          # fast | balanced | deep

[[models.extra.llama]]
id = "qwen2.5-coder:32b"
tier = "balanced"

Live provider probes are used when the daemon's environment has credentials for them: ANTHROPIC_API_KEY (Anthropic /v1/models), OPENAI_API_KEY or an API-key ~/.codex/auth.json (OpenAI /v1/models), and OLLAMA_HOST (or http://127.0.0.1:11434) for local llama tags. OpenCode is probed through opencode models, while OpenRouter uses its public catalog. A subscription-only login has no listing endpoint, so those providers fall back to the bundled catalog - the picker is never empty.

Field Details

FieldTypeDefaultDescription
bindString"127.0.0.1"Interface to bind the client WebSocket server
portu169178Port for client WebSocket connections
tokenStringauto-generated64-char hex string (256-bit) for client authentication
claude_binString"claude"Path or name of the Claude CLI binary
display_nameOption<String>(none)Human-readable node name, shown in cluster status
default_working_dirOption<String>(none)Default working directory for new sessions
arguments (alias claude_extra_args)Option<Vec<String>>(none)Extra arguments appended to every Claude session (interactive PTY and headless SDK); args already supplied by the command registry or caller are not duplicated
default_session_userOption<String>(none)OS user new sessions and durable Chats run as when no more specific session user applies; empty/absent runs as the daemon process user. On Unix, a root-effective Chat launch is rejected. Prefer a non-root account, see Chats and Teams
trusted_dirsVec<String>[]Directory roots pre-trusted for Claude PTY sessions (Unix only); see below
log_filterOption<String>"warn"Tracing filter directive. RUST_LOG env var overrides this when set
models.refresh_hoursu646Cache TTL for a provider's fetched model list
models.openrouterbooltrueConsult the keyless OpenRouter model index
models.extra.<provider>Array of tables[]Operator-supplied model entries (id, label?, tier?, context_window?)
openrouter.api_keyOption<String>(none)Node-local credential for Hive's native OpenRouter API backend; falls back to the daemon's OPENROUTER_API_KEY environment variable
cluster.node_idOption<Uuid>auto-generatedUnique node identifier, auto-generated on first cluster start
cluster.peersVec<String>[]Peer node addresses (host:peer_port)
cluster.peer_portu169179Port for peer-to-peer WebSocket connections
cluster.cluster_tokenStringrequiredShared secret for inter-node authentication
cluster.heartbeat_interval_msu641000Deprecated no-op (no leader heartbeat in leaderless clustering); retained for compatibility
cluster.election_timeout_min_msu643000Deprecated no-op (no election); retained for compatibility
cluster.election_timeout_max_msu645000Deprecated no-op (no election); retained for compatibility
cluster.min_quorumOption<usize>(none)Deprecated no-op (no quorum); retained for compatibility
cluster.advertise_addressesVec<String>[]Client-facing addresses this node advertises to peers and clients; first entry is primary
cluster.display_nameOption<String>(none)Node name in cluster context (overrides top-level display_name)
tunnel.enabledboolfalseEnables Hive Tunnel on this node (premium); requires a daemon restart
tunnel.secret_keyOption<String>auto-generatedBase64 ed25519 secret key; the public half is the node's tunnel endpoint id
tunnel.relay_urlOption<String>https://relay.vazac.dev:8443Custom iroh relay URL for tunnel connection setup and fallback

Commands

[[commands]] is the node's shell registry: the named presets the app offers in New Session → Shell, and the names hive open --shell resolves against. Each entry has a name (unique per node), a resolved program path, optional args, and an enabled flag. On first start the daemon fills an empty registry with whichever shells it finds installed, and on every start it prunes entries whose absolute program no longer exists.

toml
[[commands]]
name = "Zsh"
program = "/bin/zsh"
args = []
enabled = true

Edit them per node from the app under Settings → Cluster → Shells.

How a session's program is resolved, in order:

  1. No command sent - the daemon uses the project's own default_shell (see below) when the request names a project, then default_pty_shell, falling back to claude_bin.
  2. A registry name or program path - the matching enabled entry supplies the program and its args. Arguments sent with the request replace the entry's args rather than extending them.
  3. Anything else - treated as a command line and tokenized into a program plus arguments (quotes and backslash escapes honoured). A value naming an existing file is used verbatim, so unquoted paths with spaces are safe. Arguments sent with the request are appended. This is what the app's Claude agent and Custom... presets and hive open --shell "pwsh -NoExit" go through. A program that resolves to claude is replaced by the configured claude_bin, so a bare claude always launches the binary this node is configured with.

Whenever the resolved program is claude, the config-level arguments (legacy claude_extra_args) are merged in, whichever of the three paths produced it.

On Windows the resolved program is turned into something the OS can launch before spawning. npm installs a global CLI as several files in the same folder - claude.cmd plus an extensionless shell shim - and only the .cmd/.exe form can be started; the daemon therefore prefers an executable extension when it looks a program up on PATH, and fills one in when claude_bin points at an extensionless path. Setting claude_bin to the .cmd (or the native claude.exe) explicitly is still the clearest option, and paths containing spaces such as C:\Program Files\... need no quoting in the config file.

The registry also holds a Claude agent entry - the one that starts a Claude session - seeded on first load when the Claude CLI is installed on that node. The app lists the registry one for one, so a node whose registry has no agent entry offers no agent in New Session; add it back under Settings → Cluster → Shells → Add Shell with claude as the program. The agent entry is never selected automatically: New Session preselects the project's default shell, then the node's default_pty_shell, then the shell last used on that node, then the node's first registry entry.

default_pty_shell accepts a registry entry name (Zsh), a program (bash, pwsh, /bin/zsh) or a whole command line, exactly like a project's default_shell; the daemon resolves it through the same registry lookup, and the app matches it against each preset's name, key and program basename, case-insensitively.

Edit it per node in the app under Cluster → node → Configure → Session Defaults → Default Shell. The picker lists that node's registry entries plus Custom... for a raw command line; Auto-detect clears the setting. Any node can be configured, not only the connected one: the request is relayed to the target node, which persists it and announces the new value to the cluster. The connected node applies it live (no daemon restart); the desktop app's own Configure Local Node form still saves through the config file and restarts.

Per-Project Node

A project can pin its sessions to a cluster node. Set the node in the app under Projects → Edit → Node. Both New Session and Quick New Session honor the selected project's pinned node, regardless of which daemon the app is connected to.

In cluster mode, creating a project without an explicit node ID pins it to the daemon that receives the create request. This also covers This node during app startup, before the cluster status has supplied that node's UUID.

Hive does not silently fall back to the connected node when the pinned node is offline. It blocks session creation and reports that the selected project node is unavailable. Bring that node online or explicitly change the project's node before creating the session.

Per-Project Default Shell

A project can pin the command its sessions start with, overriding default_pty_shell for that project only. Set it in the app under Projects → Edit → Default Shell; the options are the target node's registry entries, the Claude agent, or a custom command line. Node default (the default) leaves the project following the node.

The value is stored on the project and replicated cluster-wide, so every client - desktop, web and mobile - and every headless path (hive open without --shell, scheduled tasks, the quick-session shortcut) starts that project's sessions with the same command. Since the registry is per node, pick the shell after choosing the project's node; switching to a node that cannot resolve the current choice resets the field to Node default.

Session Users and Trusted Directories

default_session_user sets the OS account new sessions run as when nothing more specific applies. Resolution order is: an explicit run_as_user on the request, then the project's run_as_users mapping for this node, then default_session_user. When all are absent, sessions run as the daemon process user (often root when hived runs as a system service, which is why a non-root default is preferred). It is editable per node from the app under Cluster → node → Configure → Session Defaults → Default User, alongside the node's default shell (see Clustering).

For a terminal session forwarded across the cluster, the destination node applies this fallback when it creates the process. If both the request and project leave the user unset, the session uses the destination node's default_session_user, not the default configured on the connected or forwarding node.

Every picker that starts work names the value it would inherit, so the fallback is never guesswork: New Session and Open session as user offer Node default (Zsh) / Project default (ubuntu) entries, and Projects → Edit shows the same on its Default Shell and Run As User fields. Choosing the inherit entry sends nothing, so the daemon resolves the chain at spawn time and a later change to a node default applies without touching the project.

Durable Chats and Notes follow the same chain minus the per-request override: the chat's project run_as_users mapping, then the node's default_session_user, then the daemon process user. New chat prints the account it resolves to. On Unix, Hive rejects a Chat launch if that account is root. Set Default User to a non-root account in the node's Session Defaults. Leaving it blank is valid when hived already runs as a non-root account.

trusted_dirs (Unix only) pre-trusts directory roots for Claude PTY sessions. On its first interactive run in an unfamiliar folder, Claude Code shows a "Do you trust the files in this folder?" dialog and waits for a keypress; a headless hive send into a fresh session would otherwise stall behind that prompt. When a claude session is about to spawn with a working directory at or under one of these roots, the daemon marks the folder trusted (hasTrustDialogAccepted) in the spawn user's ~/.claude.json before launch, so the session comes up ready for input. It writes the ~/.claude.json belonging to the resolved session user (honouring run_as_user / default_session_user) and chowns the file to them.

toml
default_session_user = "hive"
trusted_dirs = ["/home/hive/projects", "/srv/work"]

Notifications Fields

All fields live under [notifications].

FieldTypeDefaultDescription
agent_detectionArraybuilt-inPer-agent title detection rules (see below); defaults to the Claude/Codex spinner rules
agent_detection[].nameStringrequiredLabel shown in logs
agent_detection[].working_title_regexStringrequiredRegex matched against the terminal title; a match means "working"

Agent-Idle Detection (daemon-native)

The daemon watches each PTY session for an interactive agent CLI changing from working to idle, needing no CLI on the host and no per-agent hook. Claude Code uses alternating circle halves (U+25D0/U+25D1) in its OSC window title, while Codex uses an animated braille glyph (U+2800..U+28FF). Both drop the status glyph once idle, so the built-in title rules detect the transition. GitHub Copilot CLI exposes activity in the rendered terminal instead: Hive detects its live Working ... esc interrupt footer and clears the working state when that footer disappears. Codex's default title includes its thread GUID rather than a task summary; Hive uses the spinner for activity detection but discards that opaque GUID when choosing a session label or idle-notification detail. Plain shell titles do not match the title rules. The Copilot footer check is limited to the bottom of the live screen so old transcript text does not mark a session as working.

The idle transition drives the in-app desktop popup/sound. Useful human-readable titles can label the session and are persisted as its durable terminal title. Detection is always on; there are no remote push channels to configure.

To tune or add agents, override agent_detection:

toml
[[notifications.agent_detection]]
name = "claude"
working_title_regex = '^\s*[\x{25D0}\x{25D1}]'

[[notifications.agent_detection]]
name = "codex"
working_title_regex = '^\s*[\x{2800}-\x{28FF}]'

[[notifications.agent_detection]]
name = "my-agent"
working_title_regex = 'Thinking|Working'

Defining agent_detection replaces the built-in defaults, so include the Claude/Codex rules above if you still want them. To see what title your agent emits (for writing a regex), run the daemon at RUST_LOG=debug and watch the session.

In the Hive app

While Claude Code, Codex, or GitHub Copilot CLI is working, its terminal entry in the sidebar shows a rotating activity indicator. Hive removes Claude and Codex's changing title glyph from the displayed session name, so the label stays stable while work is in progress. Copilot's terminal label remains stable because its working state comes from the live footer instead of the title.

When a session goes idle the app raises a native desktop popup and plays a sound (both toggleable, per device, under Settings -> Notifications -> Desktop alerts). The finished notification uses the session's current display name when the idle transition fires, including a contextual Codex name assigned after the terminal started. The same name is stored with the durable background notification. Each terminal in the sidebar gets a bell icon next to its close button to mute idle alerts for that session on this device. These are client-side preferences.

Task Scheduler

Tasks are created and managed through the Hive app or the WebSocket task messages described in tasks.md. Each task has a schedule that controls when it runs.

Schedule Types

One-shot - runs immediately when created, then stops:

json
{ "type": "one_shot" }

Interval - repeats every N seconds after each run completes:

json
{ "type": "interval", "seconds": 3600 }

Cron - runs on a cron schedule. Hive uses 6-field expressions with seconds at the front (seconds minutes hours day-of-month month day-of-week). Seconds are mandatory - a bare 5-field expression is rejected:

json
{ "type": "cron", "expression": "0 0 * * * *" }

Cron Expression Format

The scheduler uses the cron 0.12 crate and evaluates expressions in UTC. Expressions use six space-separated fields (a seventh optional year field is allowed):

┌──────────────── second (0-59)
│  ┌───────────── minute (0-59)
│  │  ┌──────────── hour (0-23)
│  │  │  ┌──────────── day of month (1-31)
│  │  │  │  ┌──────────── month (1-12 or names: jan-dec)
│  │  │  │  │  ┌──────────── day of week (1-7, Sunday=1, or names: sun-sat)
│  │  │  │  │  │
*  *  *  *  *  *

* and ? both mean "any". Steps (*/15), ranges (9-17), and lists (1,15) are supported. Day-of-month and day-of-week are combined with AND. The shorthands @hourly, @daily, @weekly, @monthly, and @yearly are also accepted.

Common examples:

ExpressionMeaning
0 * * * * *Every minute at second 0
0 0 * * * *Every hour at minute 0
0 0 9 * * *Daily at 09:00 UTC
0 0 9 * * MonEvery Monday at 09:00 UTC
0 0 9 * * Mon-FriWeekdays at 09:00 UTC
0 0 9 1 * *First day of every month at 09:00 UTC
* * * * * *Every second

Note: All times are UTC. The scheduler checks for due tasks every 100 seconds, so the actual fire time may lag behind the scheduled time by up to one scheduler tick.

TLS Configuration

The daemon can serve over wss:// (TLS) instead of plain ws:// by setting both tls_cert and tls_key in the config file.

Enabling TLS on the Daemon

toml
# Path to the PEM-encoded TLS certificate (full chain recommended).
tls_cert = "/etc/hived/cert.pem"

# Path to the PEM-encoded TLS private key.
tls_key = "/etc/hived/key.pem"

Both fields must be set. If either is missing or the files cannot be loaded, the daemon exits at startup with an error. When TLS is active the daemon logs:

INFO TLS enabled - listening on wss://

Generating a Self-Signed Certificate (for testing)

bash
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
  -days 365 -nodes -subj "/CN=localhost"

For production, use a certificate issued by a trusted CA (e.g. Let's Encrypt / certbot).

Connecting with TLS

CLI:

bash
hive --tls --host my-server.example.com ls
# or via environment variable
HIVE_TLS=true hive ls

Environment variable:

VariableCLI FlagDefaultDescription
HIVE_TLS--tlsfalseConnect using wss:// (TLS)

App (desktop / mobile / web): there is no manual TLS toggle in the Connect view. The app auto-negotiates the scheme per connection profile - it tries wss:// when the node offers it and falls back to ws:// otherwise, caching the working scheme. Note that wss:// requires connecting by the node's Tailscale MagicDNS hostname (e.g. my-node.tailnet.ts.net), not a bare IP, since the cert is issued for that hostname. See hive cert / the web client page for provisioning a TLS cert per node.

Built-in MCP tools: when TLS is active, the daemon reads the configured leaf certificate and gives its built-in MCP subprocesses a wss:// endpoint using the certificate's DNS subject alternative name. This keeps tools such as chat context and chat title updates on the local daemon while still passing hostname verification. If the certificate has no DNS subject alternative name, Hive uses a local IP only when that exact IP is present in the certificate's IP subject alternative names. Hostname verification is never disabled.

TLS Field Reference

FieldTypeDefaultDescription
tls_certOption<String>(none)Path to PEM certificate file. TLS is disabled when absent.
tls_keyOption<String>(none)Path to PEM private key file. TLS is disabled when absent.

Note: TLS applies only to the client-facing port (port, default 9178). Peer-to-peer cluster traffic (port 9179) does not currently use TLS.

Client Configuration

The hive client is configured entirely through CLI flags and environment variables. There is no config file.

Environment Variables

VariableCLI FlagDefaultDescription
HIVE_HOST--host127.0.0.1Daemon host address
HIVE_PORT--port9178Daemon port
HIVE_TOKEN--token(required)Authentication token
HIVE_NODES--nodes(none)Comma-separated cluster node list

Precedence

CLI flags override environment variables. When --nodes / HIVE_NODES is set, it overrides --host / --port.

Example Shell Setup

bash
# Standalone
export HIVE_TOKEN="a3f8b2c1d4e5..."

# Cluster
export HIVE_TOKEN="a3f8b2c1d4e5..."
export HIVE_NODES="10.0.0.1:9178,10.0.0.2:9178"

Cluster Configuration Examples

Two-Node Cluster

Node A (10.0.0.1):

toml
bind = "0.0.0.0"
port = 9178
token = "client-auth-token"

[cluster]
peers = ["10.0.0.2:9179"]
peer_port = 9179
cluster_token = "shared-cluster-secret"
advertise_addresses = ["10.0.0.1:9178"]

Node B (10.0.0.2):

toml
bind = "0.0.0.0"
port = 9178
token = "client-auth-token"

[cluster]
peers = ["10.0.0.1:9179"]
peer_port = 9179
cluster_token = "shared-cluster-secret"
advertise_addresses = ["10.0.0.2:9178"]

Three-Node Cluster

Each node lists the other two as peers:

toml
# On 10.0.0.1
[cluster]
peers = ["10.0.0.2:9179", "10.0.0.3:9179"]
peer_port = 9179
cluster_token = "shared-cluster-secret"
advertise_addresses = ["10.0.0.1:9178"]

Two-Node Cluster

No special configuration is needed for any cluster size. Leaderless clustering has no quorum: every node keeps serving reads and writes for the state it holds even when its peer is unreachable, and anti-entropy on reconnect merges whatever diverged. A two-node cluster is just two peers pointing at each other:

toml
[cluster]
peers = ["10.0.0.2:9179"]
peer_port = 9179
cluster_token = "shared-cluster-secret"

Concurrent edits to the same project or team made on two simultaneously isolated nodes resolve last-writer-wins by updated_at when they reconnect; the older edit is dropped. Distinct objects are never lost. See Clustering for the full merge semantics.

Hive - remote AI coding agents over WebSocket.