Every message sent to the engine is automatically enriched with a structured context header. This provides metadata (message info, timestamps, custom values) so the AI can reason about the current request without extra tool calls.
You configure context in the main configuration via the context key at root or per project.
These keys are injected for every message, even without any context configuration:
| Key | Description |
|---|---|
bot.messageId |
Telegram message ID |
bot.timestamp |
Message Unix timestamp (seconds) |
bot.datetime |
Message datetime, ISO 8601 |
bot.userId |
Sender's Telegram user ID |
bot.username |
Sender's @username (if set) |
bot.firstName |
Sender's first name |
bot.chatId |
Chat ID |
bot.messageType |
text / photo / document / voice |
project.name |
Project name (falls back to project key if not set) |
project.cwd |
Resolved absolute project working directory |
project.slug |
Legacy key: path-derived slug from project cwd (/ → -); slated to be removed or replaced by the project key |
sys.datetime |
Current local datetime with timezone |
sys.date |
Current date, YYYY-MM-DD |
sys.time |
Current time, HH:MM:SS |
sys.ts |
Current Unix timestamp (seconds) |
sys.tz |
Timezone name (e.g. Europe/Berlin) |
engine.name |
Engine identifier (e.g. claude, copilot) |
engine.command |
CLI command used to invoke the engine |
engine.model |
AI model from config (only present when explicitly set) |
engine.defaultModel |
HAL default model applied (only present when engine.model is omitted; see Model defaults) |
Note: project.slug is legacy and should not be relied upon for stable identity.
Add a context object at the root level of your config (applies to all projects) or inside individual projects (overrides root per key):
globals: {}
context:
messageId: "${bot.messageId}"
currentTime: "${sys.datetime}"
buildVersion: "#{git rev-parse --short HEAD}"
projects:
backend:
cwd: ./backend
telegram:
botToken: "${BACKEND_BOT_TOKEN}"
context:
project: backend
liveTimestamp: "@{date +\"%Y-%m-%d %H:%M:%S\"}"Project context is merged on top of root — backend inherits messageId, currentTime, and buildVersion from root context, and adds project and liveTimestamp.
Three patterns are supported wherever HAL resolves values against the context map:
| Pattern | Evaluated | Description |
|---|---|---|
${expr} |
Per message / execution | Looks up expr in the full context map (implicit + configured keys), then env vars. Unresolved → empty string. |
#{cmd} |
Once at boot | Runs shell command, caches result for all messages |
@{cmd} |
Per message / execution | Runs shell command fresh for each message or cron execution |
| Location | ${expr} |
#{cmd} |
@{cmd} |
|---|---|---|---|
Config context: values |
✅ | ✅ | ✅ |
Cron .md prompt body |
✅ | ✗ | ✅ |
Cron .md frontmatter |
✅ (env + process.env only, via ${VAR}) |
✗ | ✗ |
| Skill prompt body | ✗ | ✗ | ✗ |
Custom command (.mjs) |
— (plain JS: use template literals / process.env) |
— | — |
Cron
.mdfrontmatter uses a simpler resolver (env files +process.env); it does not have access to the full runtime context map (bot.*,sys.*, etc.).
For advanced enrichment, you can provide a context.mjs hook file that transforms the context object with arbitrary JavaScript. Two hook locations are supported:
| Location | Scope |
|---|---|
{configDir}/.hal/hooks/context.mjs |
Global — runs for all projects |
{project.cwd}/.hal/hooks/context.mjs |
Project — runs for that project only |
When both exist, they chain: global runs first, its output feeds into the project hook. Both are hot-reloaded on every message (no restart needed) — so the AI engine itself can create or modify hooks at runtime.
// .hal/hooks/context.mjs
export default async (context) => ({
...context,
project: "my-tracker",
user: await fetchUserProfile(context["bot.userId"])
})- Input: fully-resolved
Record<string, string>context - Output: a
Record<string, string>— the final context passed to the engine - If a hook throws, the bot logs the error and falls back to the pre-hook context
The resolved context is prepended to the user message before passing to the engine. By default HAL also inserts a leading cwd boundary instruction based on engine.enforceCwd so the agent is explicitly told to keep file operations inside the resolved project directory. This applies to normal Telegram prompts and markdown cron prompts alike.
[System: Your working directory is /projects/backend. All file read and write operations must be relative to this path. Do not create, edit, or delete files outside this directory unless the user explicitly provides an absolute path outside it.]
# Context
- bot.messageId: 12345
- sys.datetime: 2026-02-26 14:30:00 UTC+1
- project: backend
# User Message
What files changed today?
Set engine.enforceCwd: false at globals or project scope if you intentionally want HAL to stop injecting this instruction.