Troubleshooting — hotl the agent
Look up the message you saw. Text in code is what hotl prints; find yours by grepping this file for a distinctive phrase. Run hotl doctor first for setup problems — it diagnoses most of the table below in one shot.
Startup / provider
Section titled “Startup / provider”| Message or symptom | Cause | Fix |
|---|---|---|
ANTHROPIC_API_KEY is not set. |
Provider is anthropic (the default) but no key. | Set ANTHROPIC_API_KEY, or switch to another provider: export HOTL_MODEL=openai/<model> with OPENAI_API_KEY, or HOTL_OPENAI_BASE_URL for a local endpoint. |
| You have a Claude Pro/Max plan and no API key | A subscription covers Claude Code and claude.ai, not third-party tools. | Get a key from the Claude Console (billed per token), or run a local model. Full answer: can I use my Claude subscription? |
OPENAI_API_KEY is not set (required for api.openai.com; …) |
openai provider against the default hosted URL, no key. |
Set OPENAI_API_KEY, or point HOTL_OPENAI_BASE_URL at a local server (e.g. http://localhost:11434/v1) to run keyless. |
unknown provider \X` in HOTL_MODEL` |
HOTL_MODEL isn’t anthropic/… or openai/…. |
Use provider/model. openai covers all OpenAI-compatible endpoints. |
doctor provider line shows FAIL |
Same as the above three. | Fix the env vars in the shell you’ll run hotl from, then re-run hotl doctor. |
WARNING — HOTL_OPENAI_BASE_URL is a non-loopback http:// URL and OPENAI_API_KEY is set |
Your key would cross the network unencrypted. | Use https://, an SSH tunnel, or a loopback address. The run proceeds, but the key is exposed. |
Permissions & sandbox
Section titled “Permissions & sandbox”| Message or symptom | Cause | Fix |
|---|---|---|
The agent’s action was (denied) and you never saw a prompt |
Headless (-p) or non-interactive terminal — asks auto-deny. |
Run interactively, or add an allow-rule in config.toml for the action the run needs. See configuration.md. |
| An allow-rule you wrote still prompts | The command has a shell operator, the path escapes the prefix via .., the target is a protected path, or (for bash) the sandbox isn’t enforced. |
Expected — these are the carve-outs. See permissions-and-sandbox.md. Simplify the command, or approve it by hand. |
Ask shows UNSANDBOXED |
No kernel sandbox on this host, or HOTL_SANDBOX=off. |
On older Linux, none is available; on macOS ensure /usr/bin/sandbox-exec exists. bash allow-rules are disabled while unsandboxed, by design. |
The agent’s git push fails with Permission denied (publickey), or aws/gcloud/npm can’t find credentials |
The read carve denies ~/.ssh, ~/.aws, ~/.config/gcloud, ~/.azure and the credential dotfiles to sandboxed commands. |
Load your key into ssh-agent (ssh-add) — the agent socket stays reachable and ssh then never opens a key file. For a one-off, press s instead of y at the ask. For a standing lift, add readable = ["~/.aws"] under [sandbox] and restart the session. |
Your [[deny]] rule stops the read tool but not bash cat … |
A path deny reaches shell commands only when it can be expressed at the kernel: an absolute or ~/-rooted path_prefix naming a directory that exists. A floating relative prefix (.ssh/), a prefix on a path tool, a field override, and a command-subject rule (bash prefix = "curl ") cannot be. |
Run hotl doctor. If the path is listed under containment · reads denied to shell commands, it is enforced at the kernel. If the rule is listed under not reaching shell commands, the same line names the form to write instead — usually path_prefix = "~/.ssh" rather than ".ssh/". See deny-rules. |
hotl doctor says a [[deny]] path sits inside the writable root … |
Landlock resolves the closest matching rule, so a write grant on an ancestor re-opens the read. The path is inside your working directory, TMPDIR, /dev, or a [sandbox].writable entry. |
Undeliverable at the kernel, so it is dropped rather than reported as live — the in-process deny still holds. Move the directory outside every write root if you need shell commands covered. |
A [[deny]] rule you wrote started refusing things it never used to |
Before 0.11 a [[deny]] in config.toml was dropped before the rules loaded, so only /etc/hotl/preapproved.toml could deny. It now loads, and ~/-rooted prefixes expand. |
Intended — the rule does what it said. Narrow or remove the rule if that is not what you meant. |
read/edit refuses a path under ~/.config/hotl or ~/.local/share/hotl, in every mode |
Tier A of the read carve. The session token there drives hotl’s control socket, so no approval unlocks it. | Intended, and there is no override. Open the file yourself and paste what the agent needs. |
Every bash ask says reads:open |
[sandbox].readable lifted the whole credential tier, or the probe found the carve unenforceable on this host. |
Check hotl doctor — it prints the resolved deny set and whether the probe certified the carve. |
sandbox: the read carve could not open … in hotl doctor |
The carve’s descent could not open a directory, so reads under it are denied to sandboxed commands without anyone asking for that. | Fail-closed, but usually a permissions oddity on that directory. Fix its mode, or expect commands reading under it to fail. |
⚠ PROTECTED PATH — before an ask |
The write targets a write-now/execute-later file (git hook, build.rs, ssh, creds, …). | Intended. Approve only if you meant to write that file; it can run code or grant access later. |
A command hangs about two minutes, then fails with hotl egress: "HOST" is not in [network].allow |
egress = "allowlist" and the host isn’t covered. An egress prompt went up and nobody answered inside the two-minute deadline. |
Answer the prompt (y allows the host for the session). Nothing is recorded on a timeout, so the next attempt asks again. Add the host to [network].allow to stop being asked. |
Under allowlist, an unlisted host 403s instantly with (no interactive surface …) and you were never asked |
Headless (-p, --schema) and sub-agents never get the egress prompt — by construction, not by a flag. |
Add the host to [network].allow, or run interactively. |
| You get an egress prompt for a host you just approved in a bash command | The command was approved by an [[allow]] rule rather than by you, or the URL carried userinfo (https://a.com@b.com/), or you edited the command at the ask. |
Working as intended — a rule is not a human, and in the userinfo case the host your eye read is not the host being reached. Answer the prompt. |
Every host prompts and allow looks right |
defaults = false dropped hotl’s starter list, or the entries don’t match (no ports in patterns; example.com is exact, *.example.com is the wildcard). |
hotl doctor prints the effective list split by source. |
| You want egress restriction off entirely | — | One line: egress = "open" under [network]. |
During a turn
Section titled “During a turn”| Message or symptom | Cause | Fix |
|---|---|---|
stopped — the model kept repeating: … |
Doom-loop guard: the model made the same tool call in a tight cycle. In ask mode you declined to continue; in bypass/dontask it stops on its own (nobody is watching). |
Re-prompt with a more specific instruction; the loop usually means the task was ambiguous. |
stopped — \TOOL` failed too many times in a row.` |
A tool failed 5 consecutive times (tool-failure budget). | Check the tool’s error output in the transcript; the underlying command or path is wrong. |
turn limit reached / stopped after N model steps |
The turn spent its max_turns budget (default 100 model steps; a tool round-trip costs one). |
Raise [behavior] max_turns in config.toml (or HOTL_MAX_TURNS). -1 removes the cap — the turn then ends only when the model is done, the context fills, or you interrupt. |
(context compacted — …) |
Normal: history was summarized to stay within the window. | None. If it happens too early, set HOTL_CONTEXT_WINDOW to your model’s real window size. |
session log is sealed / could not create session log |
The session log couldn’t be written (permissions, disk). | Check ~/.local/share/hotl/sessions/ is writable (hotl doctor reports this). |
preapproved rules at … refused |
The admin file isn’t root-owned, or is group/world-writable. | sudo chown root /etc/hotl/preapproved.toml && sudo chmod 644 /etc/hotl/preapproved.toml |
permissions.mode=auto requested, but this is a security-enforced build |
Expected on enforced builds; per-action asks are the build’s contract. | None. |
Minified reads and edits
Section titled “Minified reads and edits”| Message or symptom | Cause | Fix |
|---|---|---|
[minified unavailable for \X`: no grammar for this file type…]` |
The extension isn’t one of the six supported languages. | None — the plain view was served. Expected for Markdown, TOML, shell, and everything else. |
[minified unavailable for \X`: the file does not parse cleanly…]` |
The grammar found a syntax error. Either the file really is broken, or the pinned grammar is older than the syntax the file uses (a new language edition feature). | If the file is valid, the grammar is stale — that’s the note’s purpose. The plain view was served, so nothing is blocked. |
[minified unavailable …: the minifier produced output it could not verify…] |
Self-validation caught its own output: the view failed re-parse, or its structure didn’t match the source’s. A bug guard firing, not a file problem. | None needed — it degraded to the plain view, which is the designed behavior. Worth reporting with the file. |
[minified unavailable …: the minified view is N bytes, over the … cap] |
Minified reads are whole-file or nothing, and this file exceeds 200KB even minified. | None — the plain paged read was served. Use offset/limit on it as usual. |
minified reads return the whole file, so \offset`/`limit` do not apply` |
The model passed both minified: true and a paging argument. |
Expected, and self-correcting: the error names the plain read. The minified view has no line numbers to page by. |
\old_string` was not found in the minified view of `X`` |
Almost always: the text was quoted from a plain read, whose whitespace differs. | Re-read with minified: true and copy from that view — or drop minified from the edit. |
the matched text … is only formatting the minifier inserted |
old_string covered only separators the minifier synthesized, which exist nowhere in the file. |
Include a real token in old_string. |
multi-line replacements can corrupt python indentation through the minified view |
A new_string containing a newline, in a language where indentation is syntax. |
Use a plain edit (omit minified) for that change. Deliberate refusal, not a limitation to work around. |
this edit would leave \X` no longer parsing; nothing was written` |
The projected splice would break the file. Caught before the write. | Re-check old_string/new_string against a fresh minified read. The file is untouched. |
this build has no minify support |
A --no-default-features build (no C toolchain). The minified argument isn’t in the tool schema for such builds, so a model shouldn’t reach this. |
Re-issue without minified, or use a default build. |
| Savings look smaller than expected | Comment-light or small files save 10–18%; keep_comments = true (the default) is the conservative mode; JSX-heavy .tsx saves only on its non-JSX portion. |
Set [minify] keep_comments = false for 44–59%, accepting that the model reads code without the why. The per-read trailer always reports the real figure. |
MCP servers
Section titled “MCP servers”| Message or symptom | Cause | Fix |
|---|---|---|
config.toml ignored (parse error) |
Malformed config.toml. |
Fix the TOML; a bad file is ignored wholesale (fail-closed), so no servers load until it parses. |
First mcp use shows a PROTECTED PATH-style screen with a hash |
First use of that server (or its binary changed). | Expected — approving runs that binary and lets its output into context. Verify the path/hash, then approve. |
MCP call returns … timed out after 30s |
The server didn’t respond. | Check the server runs standalone; hotl won’t hang on it. |
Resume & undo
Section titled “Resume & undo”| Message or symptom | Cause | Fix |
|---|---|---|
no session starts with \X`` |
No session id has that prefix. | Run bare hotl resume to list ids, then use a longer prefix. |
WARNING — … broken parent_id chain … on resume |
The session log was edited or truncated after it was written. | The context is still loaded, but treat it as untrusted — a broken chain means tampering or corruption. |
hotl undo: no shadow snapshots found |
git wasn’t available when the session ran, so nothing was snapshotted. | Install git; hotl doctor warns when snapshots are disabled. |
hotl undo: no agent mutations to undo in session … |
The agent never mutated the workspace that session, so undo refuses rather than touch files that are only yours. | Nothing to do — this is the safe refusal. |
hotl undo: no snapshot to restore yet |
The agent has mutated, but the first capture is still staging (large repos take a moment, off the turn path). | Retry in a moment; hotl doctor shows warming → ready. |
hotl undo failed and named a locked shadow index |
A snapshot is being taken right now (the lock is legitimate), or a crashed session left a stale lock. | Retry in a moment. If no hotl session is running, remove the index.lock path the message printed. |
A file the agent created came back after hotl undo |
It shouldn’t: undo removes files created after the snapshot (the pre-undo checkpoint retains them, so nothing is unrecoverable). | Re-run hotl undo; if it persists, file a bug. |
Report a bug hotl mislabels or a fix that’s wrong: the harness treats a repeated failure as a docs/behavior bug — file it against the repo. Not covered here: live-provider quirks — no real model has driven hotl end to end yet, so novel model behavior is expected and worth reporting.