Skip to content

Latest commit

 

History

History
298 lines (233 loc) · 18 KB

File metadata and controls

298 lines (233 loc) · 18 KB

Configuration

HAL is configured via a config file in the config directory (default: the current working directory, or --config when set). The recommended way to create or complete your config is the Setup wizard — run npx @marcopeg/hal wiz, or run npx @marcopeg/hal and accept the prompt when HAL detects the config is missing/incomplete. Three formats are supported — only one per file is allowed.

Format Extension Features
JSON .json Standard JSON
JSONC .jsonc JSON with // and /* */ comments, trailing commas
YAML .yaml / .yml Full YAML syntax with native comments
File Purpose
hal.config.{json,jsonc,yaml} Main config (required)
hal.config.local.{json,jsonc,yaml} Local overrides (optional, gitignored)

Base and local configs can use different formats (e.g. hal.config.yaml + hal.config.local.json). If multiple formats exist for the same file (e.g. both .json and .jsonc), the loader exits with an error.

This section is the index for all configuration options; detailed subsections are split into focused docs.

Config files

hal.config.{json,jsonc,yaml}

Create a config file in your config directory (default: the current working directory, or --config when set). Secrets like bot tokens should be kept out of this file — use ${VAR_NAME} placeholders and store the values in .env or the shell environment instead. Keep .env out of git (see Env files).

YAML is the recommended format for examples and for configs with comments. A full key reference (all options, with links to doc pages) is reference.yaml. A short copy-paste example is examples/hal.config.yaml. JSON and JSONC are also supported — see Configuration alternatives below.

Example (YAML):

globals:
  engine:
    name: claude
  logging:
    level: info
    flow: true
    persist: false
  rateLimit:
    max: 10
    windowMs: 60000
  access:
    allowedUserIds: [123456789]

projects:
  backend:
    cwd: ./backend
    telegram:
      botToken: "${BACKEND_BOT_TOKEN}"
    logging:
      persist: true
  frontend:
    cwd: ./frontend
    engine:
      name: copilot
      model: gpt-5-mini
    telegram:
      botToken: "${FRONTEND_BOT_TOKEN}"

hal.config.local.{json,jsonc,yaml}

An optional local config file placed next to the main config is deep-merged on top of the base config at boot time. It is gitignored and is the recommended place for machine-specific values or secrets that you don't want committed.

Every field is optional. projects is a map with the same keys as the base config; each local entry is deep-merged into the base project with the same key. Keys that do not exist in the base config are invalid and cause a load error — you cannot introduce new projects from local config.

projects:
  backend:
    telegram:
      botToken: "7123456789:AAHActual-token-here"
    logging:
      persist: true

Configuration alternatives

hal.config.json and hal.config.jsonc are supported alongside hal.config.yaml. Runtime behavior is identical; the loader accepts any of the three formats (one per file).

For a full config structure use the YAML reference or example; you can convert to JSON/JSONC (e.g. with a tool or AI) if needed.

JSONC supports:

  • Single-line comments: //
  • Block comments: /* ... */
  • Trailing commas in objects and arrays

JSONC does not support: unquoted keys, single-quoted strings, or other non-standard JSON extensions.

Minimal JSON example (globals + one project):

{
  "globals": {
    "engine": { "name": "claude" },
    "access": { "allowedUserIds": [123456789] }
  },
  "projects": {
    "mybot": {
      "telegram": { "botToken": "${BOT_TOKEN}" }
    }
  }
}

Minimal JSONC example (same structure with // comments and trailing commas):

{
  "globals": {
    "engine": { "name": "claude" },
    "access": { "allowedUserIds": [123456789] },
  },
  "projects": {
    "mybot": {
      "telegram": { "botToken": "${BOT_TOKEN}" },
    },
  },
}

Environment variable substitution

Any string value in the config files can reference an environment variable with ${VAR_NAME} syntax. Values inside context blocks support the same ${expr} syntax but with a richer resolver (full context map + env) and two additional patterns (#{cmd} boot-time shell, @{cmd} message-time shell) — see Context. This works identically for all config formats (JSON, JSONC, YAML).

Variables are resolved at boot from env files next to your config (.env and .env.local). The docs use .env as the standard file; .env.local is an optional override. For full details on loading modes, precedence, the env config key, wizard file selection, and .gitignore guidance, see Env files.

If a referenced variable cannot be resolved from any source the bot exits at boot with a clear error message naming the variable and the config field that references it.

globals

Default settings applied to all projects. Any setting defined in a project overrides its global counterpart.

Key Description Default
globals.engine.name Required (unless every project sets its own). Engine: claude, copilot, codex, opencode, cursor, antigravity
globals.engine.command Override the CLI command path (engine name)
globals.engine.model Override the AI model (see Engines) (per engine)
globals.engine.enforceCwd Inject a system instruction telling the agent to treat project.cwd as the working boundary for file operations. Applies to normal prompts and markdown cron prompts. Disable only when the agent intentionally needs to roam outside the project directory. true
globals.engine.session Session mode: false (stateless), true (adapter default, omit = same), "shared", or "user". See Session configuration. "user" with OpenCode fails at boot. For Codex and Copilot, true now resolves to per-user mode. true
globals.engine.sessionMsg Message sent when renewing session (e.g. /clear) "hi!"
globals.engine.envFile Path to an env file sourced before running the engine CLI (child process only; not for HAL config substitution). Relative to project cwd; absolute paths used as-is. Active projects with a missing/unreadable file fail at boot. (none)
globals.engine.codex.* Codex permission flags See Codex
globals.engine.antigravity.* Antigravity flags See Antigravity
globals.logging Log level, flow, persist See Logging
globals.rateLimit Max messages per user per time window See Rate limit
globals.access.allowedUserIds Telegram user IDs allowed by default (entries may be numbers or strings for env substitution; after substitution they are validated and normalized to numeric IDs) []
globals.access.dangerouslyAllowUnrestrictedAccess Allow all users without a whitelist (must be explicitly true) false
globals.dataDir Default user data directory (see dataDir below)
globals.transcription.model Whisper model for voice "base.en"
globals.transcription.mode Voice transcript UX mode: confirm (buttons + confirm/cancel), inline (show transcript while processing), silent (no transcript shown) "confirm"
globals.transcription.showTranscription Legacy compatibility flag (maps to mode: inline when sticky: false) (deprecated)
globals.transcription.sticky Legacy compatibility flag (maps to mode: confirm when true) (deprecated)
globals.commands Toggle and configure built-in/system commands See Commands config and System commands

Per-engine options (Codex, Antigravity) are documented in Engines.

Session configuration

engine.session is a single value: false (stateless), true (adapter default), "shared", or "user". Full reference, per-engine behaviour, and boot-error rules: Session configuration.

Important: true means the engine default, not the same behavior for every engine. In particular, Codex and Copilot now default to per-user mode, while Cursor still defaults to shared mode.

providers

Per-engine model lists for the /model command and the set of engines available for /engine. Top-level sibling of globals and projects. Entries may include default: true (at most one per engine) to set the model when engine.model is omitted. Explicit engine.model always overrides the provider default. See Engines — Model list for full details, field reference, and examples.

Per-project providers can override the top-level list for a specific project (only for engines already listed in the base config; local config cannot add new engine keys).

Configuration shapes:

Config shape Meaning
No providers key HAL runs a fast CLI check at boot; if more than one engine is available, /engine is enabled with that list.
providers: {} or providers: (no sub-keys) Engine and model switching disabled. No boot discovery. Projects cannot change engine or model via /engine or /model.
providers: { opencode:, codex: } (empty lists) Only opencode and codex appear in /engine. Default models or CLI auto-discovery (OpenCode/Cursor) for /model.
providers with one or more engine keys Every project’s engine.name must be one of those keys; otherwise HAL fails at boot with a clear error.

When a built-in command is disabled, HAL does not intercept it. The slash command falls through to project custom commands, global custom commands, skills, and finally the agent.

Access control

Every project must have a valid access policy or the bot refuses to start. A valid policy is one of:

  • access.allowedUserIds contains at least one Telegram user ID, or
  • access.dangerouslyAllowUnrestrictedAccess is explicitly true.

When allowedUserIds is non-empty it takes precedence — only listed users are allowed, even if dangerouslyAllowUnrestrictedAccess is also true.

Format: Each allowedUserIds entry may be a number (e.g. 123456789) or a string (e.g. "123456789" or "${TELEGRAM_USER_ID}" for env substitution). After environment variable substitution, every value is validated as a valid Telegram user ID (positive integer in the official range) and normalized to a number. Invalid values (e.g. spaces, decimals, non-numeric text) cause config load to fail with an error that includes the config path and the invalid value; the process exits at boot or on hot reload.

Project-level replacement: if a project defines access (even as "access": {}), it fully replaces the global access — the two are not merged. If a project omits access entirely, the global value is inherited. An empty "access": {} at project level is a validation error because it has neither user IDs nor the dangerous flag.

This validation runs at both initial boot and after config hot-reload. A reload that introduces an invalid access config is rejected and the previous config stays active.

projects (map)

projects is an object (map) keyed by project key. Each key identifies one project and one Telegram bot connected to one directory. This key is used in logs, data paths, and errors (legacy internal name: slug).

Key format: Only letters, numbers, dashes, and underscores ([a-zA-Z0-9_-]+). This keeps the default cwd safe as a path segment when omitted.

Defaults from key: If you omit name, it defaults to the map key. If you omit cwd, it defaults to the map key (so a key backend implies cwd: "backend" unless overridden). You can still set name and cwd explicitly to override these defaults.

Key Required Description
name No Display name; defaults to the project key (map key)
active No Set to false to skip this project at boot (default: true)
cwd No Path to the project directory (relative to config file, or absolute); defaults to the project key
telegram.botToken Yes Telegram bot token from BotFather
access.allowedUserIds No User whitelist for this bot — numbers or strings (env substitution supported); validated and normalized to numeric IDs (replaces global access when set)
access.dangerouslyAllowUnrestrictedAccess No Allow all users for this bot (replaces global access entirely when set)
engine.name No Override the engine for this project (required if globals does not set one)
engine.command No Override the CLI command path
engine.model No Override the AI model (see Engines)
engine.enforceCwd No Override the HAL cwd anchor instruction for this project. When true or omitted, HAL prepends a system instruction telling the agent to keep file operations inside the resolved project cwd; set false only for intentional cross-directory workflows such as monorepos.
engine.session No Session mode for this project: false | true | "shared" | "user" (see Session configuration). For Codex and Copilot, omitted or true now means per-user mode.
engine.sessionMsg No Message used when renewing session
engine.envFile No Path to an env file sourced before running the engine CLI (child process only). Relative to this project's cwd or absolute. Missing/unreadable at boot causes boot failure for this project.
engine.codex.* No Codex permission flags (see Codex)
engine.antigravity.* No Antigravity flags (see Antigravity)
providers No Override the top-level model list for this project; entries may include default: true (at most one per list). See Engines.
logging No Override logging (see Logging)
rateLimit No Override rate limit (see Rate limit)
transcription.mode No Override voice transcript UX mode: confirm | inline | silent
transcription.showTranscription No Legacy compatibility flag (deprecated)
transcription.sticky No Legacy compatibility flag (deprecated)
dataDir No Override user data directory (see below)
context No Per-project context overrides (see Context)
commands No Toggle and configure built-in/system commands (see Commands config and System commands)

Project key (legacy: slug)

The project key (the key in the projects map) is the single source of identity and is used in log/data paths. It is not derived from name or cwd. You may still see this key referred to as slug in code and internal fields — treat slug as legacy naming.

dataDir values

Value Resolved Path
(empty) {project-cwd}/.hal/users
~ {config-dir}/.hal/{project-key}/data
Relative path (e.g. .mydata) {project-cwd}/{value}
Absolute path Used as-is

Log file paths and options are documented in Logging.

Directory structure

With a config at ~/workspace/hal.config.yaml (or .json / .jsonc):

~/workspace/
├── hal.config.yaml          (or .json / .jsonc)
├── hal.config.local.yaml    (or .json / .jsonc — gitignored, local overrides / secrets)
├── .hal/
│   ├── hooks/
│   │   └── context.mjs            (global context hook, optional)
│   ├── commands/
│   │   └── mycommand.mjs          (global command, available to all projects)
│   └── logs/
│       ├── backend/
│       │   └── 2026-02-26.txt     (when persist: true)
│       └── frontend/
│           └── 2026-02-26.txt
├── .env                     (gitignored — local secrets, do not commit)
├── .env.local               (optional gitignored override)
├── backend/
│   ├── CLAUDE.md
│   ├── .claude/
│   │   ├── settings.json
│   │   └── skills/
│   │       └── deploy/
│   │           └── SKILL.md         (skill exposed as /deploy command)
│   └── .hal/
│       ├── hooks/
│       │   └── context.mjs        (project context hook, optional)
│       ├── commands/
│       │   └── deploy.mjs         (project-specific command, optional)
│       └── users/
│           └── {userId}/
│               ├── uploads/       # Files FROM user (to engine)
│               ├── downloads/     # Files TO user (from engine)
│               └── session.json   # Session data
└── frontend/
    ├── CLAUDE.md
    └── .hal/
        └── users/

See also

Topic Description
Env files Env file loading, wizard file selection, custom env path, .gitignore
Session Session mode: false | true | "shared" | "user"; per-engine support and boot errors
Context Context injection — implicit keys, custom context, variable patterns, hooks
Commands Built-in command config — /start, /help, /reset, /clear, /model, /engine, /git
Logging Log level, flow, persist, log file paths
Rate limit Max messages per user per window (max, windowMs)
Engines Supported engines, engine config, model list, model defaults, per-engine setup