Skip to content
Hoody.com

The Hoody Agent runs inside your container, next to your files and sessions. Because the working environment lives there rather than on any one device, a turn that is streaming when you close your laptop is still streaming when you open your phone. You reach the agent from a browser tab, from ssh hoody.com, or with hoody agent in your own terminal; all three open the same running session, with nothing to install and no context to move.


When everything lives on one machine, none of it travels: close the lid and the work stops, and picking up a different device means cloning the repository again, pasting the keys again, and re-explaining what you already explained.

Hoody keeps that environment in your container instead, with the agent already running beside your files. There is nothing to install or sync on the device you are holding, no certificate, port, or VPN to set up, and no environment drift between devices. Opening a URL or typing one command returns you to the same session, the same history, and the same turn still streaming, whether from a phone, a borrowed laptop, or your own terminal.


hoody-agent is a terminal-native AI coding agent written in Go. It ships as two binaries:

  • hoody-agent-d: the daemon. It owns LLM streaming, tool execution, sessions, workflows, and memory.
  • hoody-agent: the client, a fully mouse-driven TUI built on Bubble Tea that talks to the daemon over a Unix socket.

The terminal interface is deliberate. A TUI is a byte stream, so the same Bubble Tea interface carries through your terminal emulator, over SSH, and into a browser tab through xterm.js. The browser layers its own integrations on top (clipboard images, DOM-backed web panes), so the three transports share the core interface without being pixel-for-pixel identical; that shared interface is what lets the three access paths below run on one codebase instead of three. “Terminal” does not mean keyboard-only: tabs, rows, headers, and chips are clickable, tables sort from their headers, pane dividers drag to resize, file paths open an in-TUI syntax-highlighted viewer, and URLs are real hyperlinks.


https://PROJECT_ID-CONTAINER_ID-agent-1.SERVER.containers.hoody.com

The container’s terminal kit runs the agent binary as the session’s process, with no shell underneath, and streams it to xterm.js: the same TUI in a browser tab, with full mouse support. Instance indices start at 1; agent-1 is your first agent. The web terminal’s display options, such as ?fontSize= and ?rendererType=, work on this URL too; see text draws wrongly.

You can also skip the URL: os.hoody.com signs you in with your Hoody account and opens your container’s agent full-bleed, under a slim topbar.

The agent itself also holds a sign-in to your Hoody account, which it uses to work on your containers through your account. If its shell or file tools answer not logged in, the session was bound to a container through that account while the agent had no sign-in, and the model usually reports that it cannot run anything: sign the agent in, then start a new session.

Check it with box.agent.whoami() (GET /api/v1/agent/hoody/auth/status): auth_active: true means the agent is signed in. To sign it in from a script, hand it an auth token of yours with box.agent.platform.bootstrapToken({ token }) (POST /api/v1/agent/hoody/auth/bootstrap). The token must belong to the account that owns the container, must be created from your signed-in session rather than from another token, and must carry the permissions the agent works with. When one is missing, the call is refused and the reply lists what the token lacks. The agent keeps the sign-in until the token expires or you revoke it; see Auth tokens to create and revoke them.


The interface is organized into three tab capsules:

CapsuleTabsWhat lives there
AGENTSessions, Chat, Agents, Skills, Workflows, Hooks, TODOsThe AI work surface: conversations, named agent profiles, the MCP servers they reach, reusable skills, multi-step pipelines, a triageable work inbox
FLEETComputers, Files, Scripts, Domains, ConsoleYour Hoody machines: their live terminals and filesystems, published exec scripts, custom domains, and your platform account (servers, rentals, wallet, API keys)
SYSTEMModels, Settings, Vault, Memory, LogsProviders and credentials, encrypted secrets, persistent memory, observability
Screenshot Coming Soon The Hoody Agent TUI running in a browser tab: the AGENT, FLEET, and SYSTEM tab capsules across the top, a multi-pane chat grid streaming two sessions side by side
The same TUI, whether you opened a terminal, an SSH session, or a browser tab

Most of the work happens in the Chat surface:

  • Multi-pane chat: split the surface into a live grid and watch several sessions stream at once; each pane scrolls independently and flags when it needs you.
  • Multi-attach sessions: the same session can be open in several windows and HTTP clients simultaneously. Every window streams the turn live, any of them can type or answer a permission gate, and a mid-turn joiner gets the full viewport plus the in-flight turn replayed. Closing one window just leaves; the session lives on for the others.
  • Clipboard images: copy an image anywhere and press Ctrl+V in the composer (right-click, then Paste, in the browser terminal). It attaches as a real image the model sees.
  • Paste collapse: drop a wall of text into the composer and it folds into a [Pasted text #N] chip you can click to edit. The model still receives the full content.
  • Web pages inside chat panes: when the TUI renders in the browser terminal, an “open a URL” pane overlays a genuine DOM iframe, clipped exactly to the pane’s rectangle. In a plain terminal the same pane degrades to a placeholder. It is a feature inside a terminal, not a window manager.
  • Attention notifications: when a background session needs a decision, a snackbar slides in that clicks straight to the waiting prompt.

A fresh install defaults to Hoody AI (hoody-ai/hoody-free), a passwordless relay that needs no separate model key wherever the relay is reachable and your network position is authorized; outside that path, you bring a supported model credential. The free default covers your first prompt; you can switch providers at any time. A catalogue of 100+ providers ships embedded in the binary, with credentials resolved from the environment or ~/.hoody/.env, and OAuth sign-in flows for ChatGPT Codex and OpenRouter in the Models tab.

For a Claude Pro or Max plan, Hoody implements no Anthropic OAuth flow of its own. It delegates the turn to Claude Code: the separately installed claude CLI drives the reasoning on its own login, while Hoody’s tools stay mapped in and executed by Hoody, so the session keeps the same tools, the same files, and the same confirmations. Claude Code is currently the only agent a whole session can be delegated to; the Codex, Gemini, and OpenCode CLIs are available as one-shot session tools (run_codex, run_gemini, run_opencode) whenever the CLI is installed and enabled.

A delegated Claude Code session can delegate further, to its own subagents, and they stay inside the same confinement: a subagent gets no native file or shell tools of its own, so every action it takes is a Hoody tool executed by Hoody, under the same deny-list, confirmations, and sandbox as the parent’s. Their tokens are counted toward the session’s token usage rather than going unbilled. Two limits are deliberate: a subagent cannot spawn a further subagent, and subagents run in the foreground, so the work completes within the turn you are watching rather than continuing after it. Hoody’s own background subagents and orchestrator mode are unaffected and remain available.

One further limit applies only to a delegated session: it cannot ask Hoody to run a Hoody subagent unless you have armed tools.delegated_subagents in the agent’s configuration, and the only mode that setting offers is read_only. The reason is billing rather than caution — a delegated session runs on your external model, while a Hoody subagent would run on Hoody’s model and provider account, so you arm it deliberately. The refusal names the setting. Sessions running on a Hoody model are not affected by any of this.

You can switch models mid-session, pin a model per agent profile, and pool multiple OAuth accounts per provider, rotating through them as each hits its limit. A client-side fusion engine can also vote, synthesize, or debate across several models.

Token frugality is implemented in the client code rather than left to the model: Hoody runs one generic agent and delivers phase instructions as user messages, so moving from exploring to planning never invalidates the prompt cache, and the agent can read files through a minified view that strips whitespace the model does not need, for the languages that support it.

A catalogue provider that takes an API key is set up over HTTP on the agent URL. The built-in hoody-ai provider needs no key.

  1. Store its key: PUT /api/v1/agent/providers/{id}/auth/api-key with {"api_key": "..."}. GET /api/v1/agent/providers lists the catalogue, and GET /api/v1/agent/providers/{id}/auth shows whether a key is stored, with a redacted preview in place of the key.
  2. Choose one of its models by its provider-prefixed spec, such as groq/<model>. POST /api/v1/agent/sessions with {"model": "..."} sets the model for that session only. PATCH /api/v1/agent/sessions/{id}/model with {"model": "..."} switches a live session and then tries to repin its chat agent for later sessions; only persisted: true in the reply confirms the repin. PATCH /api/v1/agent/agents/{name}/model with {"model": "..."} pins an agent profile.

An endpoint outside the catalogue, such as a vLLM server you run or a company gateway, can be added over HTTP if it speaks the OpenAI Chat Completions API, the OpenAI Responses API or the Anthropic Messages API from a public HTTPS address.

  1. Create it: POST /api/v1/agent/providers. id, base_url and models are required.

    {
    "id": "acme",
    "display_name": "Acme",
    "base_url": "https://llm.acme.example/v1",
    "models": [{ "model": "llama-3.3-70b", "context_window": 131072 }]
    }

    The reply is 201 with the provider, and the agent uses it at once, with no restart: new sessions and model switches see it, and an open session on one of its models picks it up before its next turn.

  2. Store its key: PUT /api/v1/agent/providers/acme/auth/api-key, as above. A key never goes in the create body.

  3. Use its models as acme/<model>, like any other provider’s.

id is 1 to 40 lowercase letters, digits or inner hyphens and cannot change later. The models’ prefix is the id: model_prefix can be left out, and if you send it, it must equal the id. base_url is the part of the endpoint before /chat/completions, /responses or /v1/messages. It must be an https URL with no user name, password, query or fragment, at a public address. An internal address, a name that only resolves inside a network, or a name that resolves to an internal address when a request is made is refused with 422 provider_invalid.

wire_format is chat_completions (the default), responses or messages. auth_scheme follows it: bearer for the first two and x-api-key for messages. headers adds static request headers on every call, for chat_completions only; credential headers such as Authorization are refused. Each model takes context_window and output_limit in tokens (0 or omitted means unknown, which also turns automatic compaction off for that model) and reasoning.

Send an Idempotency-Key on the create to retry it safely: a retry with the same key and body answers 201 with the provider the first try created. Without one, repeating a create that went through answers 409 provider_exists.

PATCH /api/v1/agent/providers/acme changes only the members you send. models and headers replace the current list and map, and id and model_prefix are fixed. DELETE /api/v1/agent/providers/acme removes the provider and the key stored for it; api_key_removed in the reply says whether there was one. A session that is mid-turn finishes that turn first. After that, a session still on one of its models fails its next prompt with session_unconfigured until you switch it to another model with PATCH /api/v1/agent/sessions/{id}/model, and an agent profile that names one of its models fails the same way until you change it. Built-in providers cannot be changed or removed (409 provider_builtin), and neither can a provider defined in a project’s providers file (409 provider_not_managed).


Everything that shapes the agent’s behavior is a file under .hoody/, so it can be versioned, diffed, and copied between containers:

  • Agents (.hoody/agents/*.md): named profiles that drive chat sessions, subagents, and workflow steps. Swap the active agent mid-session.
  • Skills (.hoody/skills/<name>/SKILL.md): reusable instruction sets with /<skill> slash commands. They hot-reload (drop in a SKILL.md and live sessions pick it up), and the Skills tab searches and installs from the skill hub.
  • Workflows: multi-step agent pipelines defined in JSON. Steps marked read_only hard-gate the step and every subagent it spawns to a read-only tool set. Plan, Dev Pipeline, and Codebase Map ship built in; Codebase Map is a one-click job that surveys an unfamiliar repository, and its second pass fact-checks the draft against the real code.
  • Hooks: your own shell commands on agent lifecycle events, Claude Code-compatible.
  • TODOs: a work inbox the agent shares with you. File items, let an LLM triage pass sort them, and dispatch any todo as a background orchestrator run.
  • Background subagents: spawn parallel work; the parent is notified on completion, and a live, scrollable transcript opens for each one. Orchestrator mode goes further: a delegation-first persona with no file or shell tools of its own, which does not report a change as done until an independent reviewer subagent verifies it.

Beyond file and shell tools, the agent ships web fetch, model-routed web search, image generation, grep/glob, tree-sitter code scanning with optional per-language LSP, persistent memory with hybrid recall, GitHub, and MCP servers.


Git and GitHub are built in. You link an account once, and the agent can then clone, branch, commit, sync, open pull requests and read issues against it, with the credential held by the daemon rather than pasted into a config file.

POST /api/v1/agent/github/auth/login with no body starts a device-authorization flow: it returns user_code, verification_uri, interval and expires_in, the person authorizes that URL in a browser, and POST /api/v1/agent/github/auth/login/poll with the device_code finishes the link. Send a token in the body instead and it validates a personal access token against the REST /user endpoint and stores the account that token belongs to, with no browser step. Pass host for GitHub Enterprise Server; the host is screened first, so a token is only ever sent to a public address.

Several accounts can be linked at once. GET /api/v1/agent/github/auth/status lists them with an accounts[].key of <host>/<login>, POST /api/v1/agent/github/auth/active points every later operation at one of them, and POST /api/v1/agent/github/auth/logout removes one. The account store is scoped to the realm the request arrives in. In the TUI the same four verbs are the account manager, reachable from the command palette, the Settings tab, and the repository picker on the splash screen.

Open the Git panel from the command palette and the bound repository appears across four tabs.

  • Status: the branch list, with the checked-out branch marked and each branch’s tracking ref beside it. Switch branches, commit, create or delete a branch, sync, stash and pop, and open a diff or log sub-view.
  • Worktrees: add and remove worktrees for the repository.
  • PRs: list pull requests, open one, check one out, merge, and follow its checks.
  • Issues: list and open issues.

A chip in the status bar carries the same state in one line: the current branch, how far ahead and behind its upstream it is, and how many files are dirty, or a clean marker when there is nothing to commit. Detached, rebasing and merging states replace the branch name. Every cue is written as a glyph, a count and a text form together, so the state is readable without relying on colour.

The agent can draft a commit message from your pending diff, in the TUI or over HTTP through POST /api/v1/agent/github/commit/suggest-message. It is a single tool-free model call that sends the pending diff to the selected model, built against a throwaway git index so your real index is untouched.

The same operations are 31 endpoints (28 paths) under /api/v1/agent/github/ on the container’s agent URL, browsable in the GitHub API reference: clone, repo/reconnect, branch, branch/switch, branch/delete, branches, commit, commit/push, commit/suggest-message, sync, stash, stash/pop, diff, log, status, identity, repos, worktrees, worktrees/remove, pr, pr/checkout, pr/merge, issues, and the five auth routes.

POST /api/v1/agent/github/clone takes repo as owner/name or an HTTPS GitHub URL, resolves it against the active account’s host, and refuses a URL whose host does not match. POST /api/v1/agent/github/sync runs fetch, then pull, then push as one operation, stopping at the first step that fails so a push never lands on a half-synced tree; direction narrows it to pull-only or push-only. Routes that resolve a working directory read it from the X-Hoody-Cwd header, and X-Hoody-Container binds the operation to a remote container, which the daemon probes for the exec transport before it runs anything there.

The credential never appears in a command line. Every git and gh invocation gets a full-replacement environment carrying GH_TOKEN, or GH_ENTERPRISE_TOKEN on Enterprise Server, and the argv holds only the variable’s name. It is not written to the session record, the logs, or the daemon’s memory, and output is scanned for the token’s own value before it leaves the daemon, so a server that reflects the credential back in a branch or commit field cannot leak it through a response. The same environment points GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM at /dev/null and disables hooks, so a credential helper configured in the repository cannot read it either.

One case is different, and it is a deliberate trade. An interactive clone from the TUI writes the host’s token into a mode 0600 credential store inside the new checkout’s .git directory and points a host-scoped credential.helper at it, because that is what makes a plain git push work from the agent’s own shell. The token is on the repository’s filesystem from then on, readable by the agent, and scoped to that host over HTTPS. Re-linking an account after its token expires re-points the copies, and a copy that belongs to a different linked account is left alone rather than overwritten. Logging out tries to delete them and says so honestly: the purge reaches only checkouts a live session is bound to, and the reply comes back with "credential_purge": "partial". A checkout nothing is bound to, or one in a stopped container, keeps its copy. Revoking the token on the provider is the only complete remedy. A clone issued over HTTP or the SDK does not persist a token by default. Setting persist_credentials: true on POST /api/v1/agent/github/clone writes the same credential store, and POST /api/v1/agent/github/repo/reconnect always writes or refreshes it. No other HTTP route persists a token.


Model Context Protocol servers bring third-party tools (Jira, a database, your own internal service) into a session beside the agent’s built-in ones. The GitHub server appears below as the worked example because its config exercises every field, not because you need it: cloning, branching, committing, syncing and pull requests are built in, and that path keeps the credential in the daemon instead of in settings.json. They arrive namespaced as mcp__<server>__<tool>, and the model can call them the moment they connect. The client is built on the official Go SDK and speaks every spec revision from 2024-11-05 through the current 2026-07-28, negotiating with each server automatically; there is no revision to pin or track by hand.

Adding a server does not require editing a file. The Agents tab has a second mode, MCP servers: press M on the Agents tab, click it on the mode strip above the list, or press M from the Skills tab to trace where a tool came from. M again, or esc, goes back to Definitions (tab does not: the tab switcher claims it before either surface sees it). The mode lists every configured server with live state: connected or not, the revision it negotiated, how many tools it advertises, the process id of a stdio child, and the tail of its stderr when one dies.

KeyDoes
space / enterEnable or disable the selected server
d / deleteArm removal: press it again to confirm; any other key disarms it
iImport servers from a pasted config
RRe-read the config from disk and reconnect (rate-limited to once every 5s per session)
rRefresh the list
M / escBack to Definitions

Removal asks twice on purpose: deleting an entry destroys its env and headers values, and the read surface never returns those, so there is nothing on screen to retype them from. The arm is cancelled by anything that changes what the next press would act on: moving the selection, leaving the surface or the tab, or a refresh that reloads the list.

Import accepts the config you already have. Paste a block straight out of Claude, Cursor, or VS Code; all three dialects are recognized, and a document that mixes them is refused rather than guessed at. ctrl+p previews exactly what would be written, with credential values stripped; ctrl+s writes it. Imported servers land disabled, so nothing runs before you have read the config and switched it on.

All of it applies to sessions that are already open. Disabling or removing a server cuts it off immediately, mid-turn, before the next call is admitted. A call that has already been sent to the server is allowed to return its result, since the server has already acted on it; nothing new is admitted after that point. A healthy server you did not touch is never restarted by an unrelated edit.

Servers live under mcp_servers in any settings layer (~/.hoody/settings.json, then the project’s, then local), and the layers merge field by field, so a project file can add one restriction without restating the whole entry.

A realistic starting set, where each entry shows a shape that behaves differently:

{
"mcp_servers": [
{
"name": "filesystem",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
"require_confirmation": true
},
{
"name": "github",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}" }
},
{
"name": "git",
"command": "uvx",
"args": ["mcp-server-git", "--repository", "/workspace"]
},
{
"name": "sentry",
"type": "http",
"url": "https://mcp.sentry.dev/mcp",
"headers": { "Authorization": "Sentry-Bearer ${SENTRY_TOKEN}" },
"allowed_tools": ["search_issues", "search_events"]
}
]
}

Each entry demonstrates one decision:

EntryWhat it shows
filesystemPass the narrowest path that still works. That argument is the server’s own directory-access control, not Hoody’s: the process still runs with your privileges, so treat it as tidiness, not confinement. require_confirmation is the part Hoody enforces, and it gates every call from that server, reads included.
githubThe variable the server expects (GITHUB_PERSONAL_ACCESS_TOKEN) and the one holding your secret (GITHUB_TOKEN) need not match: the left side is the server’s name for it, the right side is ${VAR} read from the daemon’s environment. -e NAME with no value makes Docker forward it from the environment Hoody built, so the token never appears in args.
gitNot everything is npx. uvx (Python) works identically; Hoody runs a command and does not care which ecosystem it came from.
sentryA remote server: type: "http", no subprocess, nothing installed. Also a warning against pattern-matching: Sentry requires Sentry-Bearer, not Bearer, because plain Bearer is reserved for MCP OAuth tokens there. Read the server’s own auth docs rather than assuming. allowed_tools is an exact-name filter, so a near-miss name silently exposes nothing.
  • type: stdio (the default), http/url for Streamable HTTP, or sse for the deprecated 2024-11-05 HTTP+SSE transport. All three are implemented; an unrecognized value is a config error, not a silent fallback to stdio.
  • name: letters, digits, _ and -; no __ (that separates the tool name), and neither hoody nor mcp.
  • enabled: absent means enabled. A disabled server keeps its config and its credentials; it is simply not connected and not advertised.
  • allowed_tools: absent means no restriction. Present and empty means expose nothing.
  • require_confirmation: route every call from this server through the confirm gate. A tool the server itself marks destructive forces one anyway.
  • env (stdio) and headers (URL): ${VAR} is expanded against the daemon’s environment, and only in these two fields. command, args, and url are used verbatim: a literal secret written there is stored in clear text in settings.json, and a ${VAR} written there is not substituted. What that breaks depends on the field: in args the server receives the six literal characters ${VAR} and usually fails to authenticate with no obvious cause; in command the daemon tries to execute a program literally named ${VAR} and the server never starts at all; in url the literal reaches URL parsing.

A stdio server starts with a scrubbed environment: no provider API key and no Hoody platform token reaches it by inheritance. Naming one is how you share it on purpose: "env": {"GITHUB_TOKEN": "${GITHUB_TOKEN}"} reads the daemon’s own value and hands it to that one server.

One rule is worth knowing before splitting config across layers: if a later layer changes what a server is (its type, command, args, or url) without also supplying env/headers, those are cleared rather than inherited. Credentials are never carried onto new code or a new endpoint.

The same surface is scriptable under /api/v1/agent/mcp/… on the container’s agent URL: GET/PUT/DELETE /mcp/servers, plus POST /mcp/servers/enable, /mcp/import, /mcp/parse and /mcp/reconnect. Writes are two-step: POST /mcp/write-intents mints a single-use nonce bound to one operation and one settings layer, and the write presents it together with expect_hash, the content hash you last read.

Both are required on every write (upsert, delete, set_enabled and import alike), so a concurrent edit comes back as a conflict instead of a silent overwrite. Note the spelling: the route is /mcp/servers/enable, but the op you mint the nonce for is set_enabled. Anything else is rejected before a nonce is issued. A first write into a settings file that does not exist yet is no exception: GET /mcp/servers and POST /mcp/write-intents report the empty-array hash for a missing file, and you pass that. Stating that you expect nothing there yet is a precondition like any other, not a reason to leave the field off.

POST /mcp/probe, which connects to a candidate config, reports what it offers, and saves nothing, is human-only and refuses a machine caller, because probing starts a process or reaches out to a URL of the caller’s choosing.


Connect the agent to the Hoody platform and the FLEET capsule becomes active:

  • Manage remote containers and their filesystems (Computers, Files), and run tools on the remote container instead of locally.
  • Drive multiple live container terminals in a grid, beside each machine’s details (Computers).
  • Handle the account side of the platform without leaving the TUI: servers, rentals, wallet, account, API keys (Console).
  • Publish exec Scripts as instant HTTP endpoints and connect custom Domains.
  • The agent gains fleet tools: discover containers, author and run exec scripts, supervise long-running programs, schedule cron jobs, send notifications. They exist only when the platform is reachable, so a session without platform access carries none of their prompt cost.
  • Browse a realm-scoped Vault with optional client-side AES-256-GCM encryption: an entry saved with encryption on is encrypted by the TUI, so the daemon only sees its envelope; an entry saved with encryption off passes through the daemon as plaintext.

The entire agent (sessions, workflows, skills, models, memory, logs) is also an HTTP surface under /api/v1/agent/... on the container’s agent URL. It is described by a generated OpenAPI 3.1 spec, browsable in the Agent API reference; the daemon can also write it offline with hoody-agent-d --generate-openapi. The URL you open in a browser tab is the same one your scripts, webhooks, and other agents call; there is no separate API mode to set up.

Sessions and prompts are the centre of that surface. Seven smaller families sit beside them, each with its own reference page:

  • Loops (/sessions/{id}/loops): recurring turns on a session, bounded by an interval, a max_runs, a stop_when predicate, and cost and wall-clock budgets. run-now fires one immediately.
  • Background tasks (/sessions/{id}/tasks): the catalogue of subagent tasks a session has spawned, each one’s transcript, and cancellation for one task or all of them. The read needs no live session: it merges the running registry with the persisted store, so a task that finished and left memory is still listed.
  • Jobs (/jobs/{id}): status, result and cancellation for anything dispatched asynchronously, including workflow runs and long tool calls. Cancelling a dispatch job cancels that one turn.
  • Headless runs (/headless/runs): one full agent loop over a throwaway session, returned as text, JSON or a stream. The default form answers 202 with a job_id you poll through the jobs routes; format: "stream-json" streams the run over SSE instead.
  • Statistics (/statistics, /usage/by-account, /usage/by-model): usage rolled up across sessions, per account and per model. /statistics takes ?scope=cwd by default, all for everything; the two /usage/* rollups always cover the whole daemon and take an optional since.
  • Discovery (/realms, /containers): the realms and containers a caller can bind a session to. Read-only, and not fleet management.
  • Memory (/memory): what the agent has learned and kept. Search and browse it, save, edit and delete single items, erase everything held for one project, and read a status that tells an empty store apart from one that is switched off or whose host is unreachable — three states that otherwise answer identically. Erasing a project is deliberately two calls: ask for a write intent, then present it on the delete, so a single stray request cannot wipe a project. When the rows are gone but a follow-up step could not be confirmed, the reply says the erasure is incomplete and names the step, rather than reporting a clean result. Retention is set per item in whole days; a fractional or quoted value is refused rather than stored as permanent, and so is one beyond the longest expiry a store can hold — about 292 years — rather than being quietly shortened to something else. Ranking strength is a whole number from 1 to 10; when creating an item, leaving it out or sending 0 selects the default, and an edit accepts 1 to 10 only. Anything else is refused, never silently replaced. Time order in the store is exact to the nanosecond: a timeline around an item always contains that item, and two items recorded in the same instant keep one stable order. The first start after an upgrade brings older entries to the same precision once; an older agent build cannot open a store a newer one has upgraded, and says so instead of running against it.
Terminal window
# One-shot: stream a full agent turn from your shell
hoody agent prompt "Run the tests and fix the first failure" -c $CONTAINER_ID
# Unattended: auto-approve confirm gates for the life of the turn
hoody agent prompt "Upgrade the deps and rerun the suite" -c $CONTAINER_ID -y
# The generated subcommands cover the whole surface
hoody agent sessions list -c $CONTAINER_ID

Every session carries an approval policy, read at GET /sessions/{id}/approval and changed with PUT on the same path. The mode is default or always. In default mode the agent asks before the actions it reserves for a person and proceeds with the rest. In always mode every side-effecting tool call waits for one explicit decision that names the gate it answers, except the agent’s own memory saves and TODO updates; reads are never gated. Setting locked: true freezes the mode for the life of the session and cannot be undone: the YOLO toggle, per-tool allow rules, remembered approvals and the auto-approve headers all stop working, and each attempt is refused with a 409 rather than dropped silently. prompt:sync and prompt:stream with policy=auto_approve are refused the same way on an always session.

The read returns an ETag that carries the policy revision. Send it back as If-Match on the PUT; a stale revision answers 412 with the current one, so two clients cannot overwrite each other’s change without seeing it. A session with a turn or a parked gate in flight refuses the change with 409 until it is idle.

Per-tool rules live under /sessions/{id}/approval/rules/{tool} and take one decision each, allow or deny. A deny is always accepted. An allow is refused on an always or locked policy, where allow rules are never consulted.

Terminal window
# Read the policy; the ETag is the revision
curl -si "$AGENT/api/v1/agent/sessions/$SESSION/approval" | grep -i etag
# Require an explicit decision for every side-effecting call, and lock it in
curl -X PUT "$AGENT/api/v1/agent/sessions/$SESSION/approval" \
-H "Content-Type: application/json" -H 'If-Match: "3"' \
-d '{"mode": "always", "locked": true}'

On an always session, answering a gate takes a lease as well as a decision. POST /sessions/{id}/approver-lease returns a capability bound to that session and to the caller, with a visible expiry. Every confirm then presents it in the X-Hoody-Approver-Lease header, and the agent checks it at the moment the decision is consumed. One holder exists at a time: a second acquire answers 409 unless it presents the current capability in the same header, which fences the previous holder; replace: true on its own does not. This is what keeps two clients attached to one session from both answering the same gate, and why a decision from a client that lost the lease is refused instead of applied. Renew the lease with PATCH and release it with DELETE on the same path. The capability is returned once, at acquisition, and lives in memory: after the agent restarts, the approval read reports held: false and the client acquires a new one.

POST /sessions/{id}/turns dispatches a turn and answers 202 with a receipt (turn_id, job_id, state, stream_url, result_url) once the agent has recorded it durably. Send an Idempotency-Key header and a retry with the same key returns the same receipt with duplicate: true; the turn never runs twice. The same key with a different request is 422. A session that already has a turn running or a gate parked refuses with 409 and records nothing, so a refused send is safe to repeat once the session is idle. POST /sessions/{id}/messages returns the same turn_id in its response.

GET /sessions/{id}/turns/{turn_id} returns the durable record of a turn for a live or dormant session: its state, and once it ends, its outcome and error_code. A turn cut off by a restart ends as interrupted with effects_may_have_occurred set, and the agent never re-runs it on its own.

Terminal window
# Dispatch once; repeating the same command returns the same receipt with duplicate:true
curl -X POST "$AGENT/api/v1/agent/sessions/$SESSION/turns" \
-H "Content-Type: application/json" -H "Idempotency-Key: deploy-2026-09-19-1" \
-d '{"text": "Run the tests and fix the first failure."}'

GET /sessions/{id}/state returns one consistent snapshot to rebuild from after a disconnect: the stream cursor to resume from (incarnation and seq), a coarse state (idle, running, parked or closed), the approval policy and rules, the queued turn ids, and the parked gate, if any, with the identity a confirm must name. A client that dispatched a turn and then disconnected can keep the session open with an attachment lease. POST /sessions/{id}/attachments (15 minutes by default, at most 60) keeps a live session and its parked gate from the idle sweep that otherwise closes it after five minutes without a stream attached; renew it with PATCH and release it with DELETE. Both kinds of lease live in memory and end when the agent restarts; /state reports what survived.

GET /api/v1/agent/version lists the API version and one capability row per feature, each marked supported (this build knows it) and available (its route is mounted here), so a client can check for these routes before depending on them.


Inside a Hoody container the agent arrives installed: the binary and daemon are present, the daemon starts automatically the first time you reach the agent, your sessions and files stay with the container, and the TUI is reachable the moment you are, from a browser, SSH, or the CLI. There is no setup step and no policy file to write.

Command sandboxing and confirmation defaults are on without configuration. This is the agent’s own sandbox for the commands it runs, and it is separate from the daemon’s optional program sandbox, which confines a whole program and refuses to launch when a mechanism it needs is missing. Command execution uses what your OS provides (Landlock on Linux, with a bubblewrap fallback; Seatbelt on macOS), and where no such layer is available the agent logs a warning and runs the commands unsandboxed rather than refusing them. The daemon’s program sandbox does the opposite and refuses to launch. Confirmation rules and terminal-output sanitization stay active with nothing for you to configure. The agent sends no analytics, crash reports, device identifiers, or other telemetry network calls; it still contacts the model providers, web services, OAuth endpoints, and optional Hoody platform services that you choose to use.