Run your container from a chat app
Section titled “Run your container from a chat app”You are on a train with a phone, and you need to know what your container is doing. Which containers are up, what the last events were, whether a service is answering. The question is thirty seconds long. Getting to a laptop is not.
hoody-bot lets you control Hoody from a chat app such as Telegram, and registering one into the container closes that distance. You do it once from the CLI. After that you sign in from a direct message and the chat app reaches the same container the CLI reaches, with the same commands under the same names. The kit is written for chat apps in general and Telegram is the first one it supports, so where this guide names Telegram it means that chat app rather than the design.
SSH, the hoody CLI, the web UI and a chat app are four ways into the same container. This one adds no account, no permission model and no identity of its own. Everything it does, it does as you.
One-time registration
Section titled “One-time registration”Create a bot with Telegram’s BotFather, then hand its token to the container once.
If you have not made a Telegram bot before: message @BotFather, send /newbot, answer with a display name and a username ending in bot, and copy the token it replies with into $BOT_TOKEN. Telegram walks through that, along with privacy mode, group access and the profile pieces only BotFather can change.
# The token travels once, in the request body over HTTPS, under your own bearer.# --token-stdin keeps the token out of the process list and shell history.printf '%s' "$BOT_TOKEN" | hoody bot create --container "$CONTAINER_ID" \ --channel telegram --token-stdin --label "ops bot"# Bot registered.
# A new registration is stopped. Read its id, then start polling.hoody bot list --container "$CONTAINER_ID"hoody bot start "$REGISTRATION" --container "$CONTAINER_ID"# The same call the CLI makes: the bot kit's own route on the container.curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-bot-1.$SERVER_NAME.containers.hoody.com/api/v1/bot/registrations" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"channel": "telegram", "token": "'"$BOT_TOKEN"'", "label": "ops bot"}'
# A new registration is stopped. Read its id, then start polling.curl "https://$PROJECT_ID-$CONTAINER_ID-bot-1.$SERVER_NAME.containers.hoody.com/api/v1/bot/registrations" \ -H "Authorization: Bearer $HOODY_TOKEN"curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-bot-1.$SERVER_NAME.containers.hoody.com/api/v1/bot/registrations/$REGISTRATION/start" \ -H "Authorization: Bearer $HOODY_TOKEN"Registration stores the token encrypted, along with the registration’s own details, and creates the row stopped, so the bot answers no message until you start it. Run hoody bot list for the registration id and hoody bot start <registration> against the same container. A started registration resumes polling on its own after the container or the kit restarts, because the state is stored rather than held in the process.
--channel and --token are both required, and --label is optional. The --token here is the chat platform’s bot token and belongs to the subcommand, not to the --token that sets your Hoody credential; write it after bot create, as above, and never before it. Put the token in a shell variable rather than typing it literally, so it stays out of your shell history; while the command runs it is still visible in your own process arguments. bot is a kit command group, so like every other kit command in this guide it needs a target container: pass --container (or -c), or set HOODY_CONTAINER once in your shell.
The kit checks the token against Telegram, encrypts it with a key generated inside the container on first boot, and stores it. Within the kit the token is never written to a log, never echoed back to you, and never appears in a chat message. The same command group lists registrations, inspects one, starts and stops a registration, reports its health, and deletes it when you are done.
The kit fetches updates from Telegram rather than being called by it. There is no webhook route, so chat traffic never arrives as an inbound request. That is a statement about how updates are delivered and nothing more: the container still serves the login page, the kit’s pages and its management routes, and those have their own admission rules.
Sign in from a direct message
Section titled “Sign in from a direct message”With the registration started, send /login in a direct message. The bot offers three buttons, and every one of them refuses to work outside a direct message. If you are already logged in, /login offers Switch account or Stay logged in. Switch account asks you to log out first; confirming that ends the session and then shows the three ways in.
The bot asks one question at a time, in the chat: your email, then your password, then your two-factor code if the account has one, then a second fresh code for the token mint.
Each answer is added to the redaction set so it cannot reappear in a log or a message, used once for the sign-in and the token mint, and then dropped. Nothing is written to disk. An unfinished form expires two minutes after it opened, and the next maintenance sweep frees what it held. The bot deletes each message as soon as it has read it.
That deletion is best effort, and the first prompt says so: Telegram stored the message on its own servers the moment you sent it. When it fails here, the bot tells you the message is still there and asks you to delete it yourself. Where a credential is refused outright, in a group, a topic, a channel, a guest context or a business connection, the warning also asks you to rotate it: “I could not delete that message, so it is still there: please delete it yourself and rotate that credential.” If that matters to you, use the login page instead.
The bot sends a one-tap link to a form the kit serves from your own container, with the link preview suppressed so nothing fetches that address on your behalf before you do.
You fill in the same values on the page. The password and the codes stay out of the chat entirely.
The link carries a single-use code, good for two minutes and bound to your chat identity. Opening the page does not spend it. A successful sign-in does, and a failed one hands it back for another try, up to three attempts before it is burned. A second submission that arrives while the first is still being processed is refused rather than raced. Treat the link as a credential while it is live: at first use, holding it is the only thing standing between someone else and your login form, so do not forward it.
Paste an API token you already hold, in a direct message.
The kit checks that the token is enabled, that it carries an expiry, and that it can list its own realms, containers and account, then stores it encrypted and uses it as it is. A token with no expiry is refused, because the kit never stores a permanent credential. Nothing else is checked: the lifetime, the realm set and the permissions are whatever you minted.
The kit holds no parent for a pasted token, so it cannot revoke it. /logout forgets it and prints the hoody auth tokens delete <id> command for you to run.
If the account has two-factor authentication, a code is asked for twice on the first two paths, and that is deliberate rather than a bug. One completes the login. The other is submitted separately when your token is minted, because that step runs its own one-time-code check.
Signing in on the page also gives that browser a 30-minute session for this container’s bot pages, which is what the secret-form links open.
What the bot holds
Section titled “What the bot holds”Signing in with a password, in the chat or on the login page, mints two tokens for you and only for you. A token you paste in is stored and used as it is, and the two paragraphs below do not describe it.
The first exists to mint the second. It is stored encrypted, never used for an ordinary call, and expires in 30 days. The second does every piece of work you ask for.
The minting token is created with the full-access permission set for user API tokens and token creation turned on, and the working token is minted without a list of its own and inherits that tree, bound to the realms your account held at login rather than to the whole platform. That set is fixed, not a copy of your own account’s permissions, and it grants only what a user token can hold on your own account. That covers container and project lifecycle, the container actions and features, billing and wallet permissions, server rental, SSH keys, firewalls, proxy aliases, storage shares, event access, token creation and the vault. The kit reads that tree from the platform rather than writing one out, because a hand-written list would silently drop every permission Hoody adds after the kit was written.
An account with no container is told: “You have no containers yet — create one first, then run this again.” Creating a container is a generated write command you can run by name from the chat, but the bot does not open it for you.
These are tokens, not sessions, so they survive a password change and a “log out everywhere”. The two ways to end them are in Sign-out and revocation below, and Chat Access covers the token lineage and its limits in more detail.
Find a command and run it
Section titled “Find a command and run it”Operations are reachable by name from the chat. /menu walks the generated menu tree, and a command’s menu page offers Continue, More options and Back: Continue runs it or opens its form, More options shows a parameter reference shortened to fit one message, and Back returns to the menu. When the whole reference will not fit, More options leaves out the details of the optional inputs, or their names as well, and says how many it left out; /help <command> prints the full reference across as many messages as it takes. /search <text> ranks commands by name, alias, button label, title and description, and choosing a result opens the command’s page rather than running it. /help <command> prints that command’s arguments from the same generated document. /call <command> key=value runs one directly, and a command whose request body is a single value rather than a set of named properties takes @=<json>, which claims the rest of the line. There is a single shortcut, /ps, which lists containers.
Six lines cover the shapes you will meet:
/ps a read, answered at once/call containers_list limit=5 the same read with an argument/call containers_copy id=CONTAINER target_project_id=PROJECT a write, so it asks for a tap/call files_downloads_url directory=uploads download=https://example.com/f.tar a read that leaves the platform, so it asks for the typed phrase/call containers_env_bulk_set id=CONTAINER @={"LOG_LEVEL":"debug"} a whole JSON body, claiming the rest of the line/call containers_copy missing required fields, so the form opens/recent shows the last commands you ran, each by its label and the target it ran on, and never the arguments, because an argument can be a path or something pasted into the wrong field. Its buttons open the command rather than run it, and running one again collects its arguments afresh.
Two things shape what happens when you run one. Every command carries a risk class, and the class sets what you are asked first: nothing for a read, a tap for a write or an action, a typed phrase for anything destructive or dangerous, and a typed phrase as well for the six reads whose execution class leaves the platform or for a command that declares its own requirement. Your own token bounds the rest, which is why a command can run for one account and be refused for another.
The phrase belongs to the prompt in front of you rather than to the command. It is the command’s slash name with its punctuation removed and in capitals, a dash, and an eight-character hexadecimal tag the prompt prints. The prompt is one message: the phrase in its text, a disabled hint that repeats it, and a Cancel button. No tap can confirm it. Type exactly what the prompt shows, because a generated command’s slash name carries underscores and a phrase typed with them matches nothing. Every confirmation prompt has Cancel, and /cancel does the same; a phrase typed after that runs nothing. A prompt for /containers_env_delete shows a phrase like CONTAINERSENVDELETE-3F9C1D0A. One phrase read off an earlier prompt will not confirm this one, and if what you type fits more than one prompt you have open, nothing runs and the bot asks you to reply to the prompt you meant.
A confirmed write, action, destructive or dangerous command runs at most once. If the chat app redelivers your tap, or the bot restarts before its reply lands, the operation is not repeated: you are told that it may already have run, that it did not run, or that it ran and its result may not have reached you. A confirmed read follows the read rule instead and may be repeated after a restart.
After a notice that a command may already have run, hoody bot logs list <registration> shows what the bot recorded for it. The resource itself, or the account’s event list, says whether the operation took effect.
When a command needs arguments you did not supply, the bot asks for them one at a time rather than rejecting the whole line. The required fields are asked, and so are optional fields that hold a secret; other optional values go through /call name=value. Cancel is available at every step, Back appears from the second question on and returns to the previous one, and a form left alone for 15 minutes expires.
A secret is answered in the browser rather than in the chat, because a secret typed into a chat has reached the channel’s storage before the bot sees it. The step sends you a link to a form the kit serves on your own container, and the chat form waits there until you submit it, then asks the next question. A value the API accepts written more than one way, such as a window id in decimal or in hex, is given once in whatever spelling you have. A field that takes a file upload cannot be answered from a chat yet, which is why the four commands that require one are not offered.
Results come back shaped rather than as a wall of JSON: a single record, a table, or plain text. An answer combines its result and its actions when they fit in one message, and a long answer continues across messages. A command that answers with bytes sends them as a document carrying the answer’s buttons, even though a field that takes a file upload cannot be answered from a chat. Menus and option lists show up to ten entries, and fewer where the channel’s button budget is smaller; how much of a result you get is set by that command’s own output limit or by the API’s pagination rather than by that ten. /plain on swaps buttons for numbered options if you prefer to answer by typing.
The realm and the target container
Section titled “The realm and the target container”The bot reaches your realms the way the CLI does with --realm. If your account holds a single realm you never see this: it is scoped to it silently. With several, every realm is in scope after you sign in: lists that span realms are combined, while other reads use the resource or container they address, run once for the account, or ask which realm to use. When some realms do not answer a read, the bot shows what it has, says how many did not answer, and offers Retry and Narrow to one realm. /realm <alias or id> narrows the scope to one realm, /realm on its own lists them and marks the narrowed one if you have narrowed, and /realm all restores the whole set. Narrowing needs no new login; a realm you gain after signing in does.
A realm you have named shows as the name followed by part of its id, and when two names would still collide, both show the whole id. A realm you have not named shows as its id. You can type the name, the realm id, or the name of a container in that realm, and if more than one realm answers, the bot asks instead of guessing. The words all, none, every, any and global are reserved and never resolve as a name.
Nothing is selected at sign-in. A command that runs inside a container then offers a button per container, up to twelve, and runs itself on the one you pick; past twelve it points you to /use <container name> for the rest. /use <container name or id> selects one directly, and /whoami shows how you signed in, the target container, the realms this session covers, the realm commands run in, the token ids and when the session expires; it never shows an account id. /use points at a realm as well. The selection belongs to your chat identity rather than to one conversation, so a /use typed in an allowlisted group moves the target in your direct messages too, and a confirmation for a command run with no target says it has none.
If you signed in by pasting a token, the realms /use and /realm offer are that token’s own realms, which can be narrower than your account’s.
The current scope and the target a command names decide the realm it runs in. A kit command runs in the selected container. With every realm in scope, a command that names a resource by id runs in that resource’s own realm; once /realm has narrowed the scope, commands stay in the realm you named. A command that changes something is never sent to a guessed realm. The target is not the authority: your permission set is realm-wide and comes from the realms your account held at login, so /use changes where a command points and never what you may do.
Groups and allowlists
Section titled “Groups and allowlists”Leave the chat allowlist unset and the bot serves direct messages only. That is the default for a reason: a read command typed in a group posts one person’s account data into the room. A command sent in a group that is not allowlisted gets one refusal, “This group isn’t allowed to use me. Message me directly, or ask the bot’s owner to allow it.”, with a Message me directly button; ordinary conversation in that group gets no reply.
hoody bot policy update <registration> sets the allowlists when you want a group served. The flags are --mode single|multi, --allowlists-users <id,...> and --allowlists-chats <id,...>, and --mode is required on every call; pass the literal null to --allowlists to clear the stored block.
hoody bot policy update "$REGISTRATION" --mode multi \ --allowlists-chats "$GROUP_CHAT_ID,$YOUR_DM_CHAT_ID"The chat list is exhaustive once it is non-empty, and a direct message’s chat id is the user’s own id, so a list naming only a group also stops every direct message. Your own direct-message chat id is your channel user id, which hoody bot logs list records as the actor of everything you have sent. An empty list admits no chat at all. Both allowlists are enforced before a chat user is looked up and before a browser login is admitted, and a refusal is written to the audit log.
This design expects the bot to have Telegram’s privacy mode enabled, and that is a configuration on your bot rather than something the kit enforces. With it on, Telegram limits which group messages it delivers, under its own privacy-mode rules. Of the group text that does arrive, the kit handles commands addressed to it, mentions and replies, and ignores the rest. Read Telegram’s rules before you add the bot to a group that discusses anything you would not want it to receive.
Credentials are never accepted in a group. /login there answers with a button that opens a direct message, and a pasted token is refused. The bot tries to delete that message; if it cannot, it says so and asks you to delete it yourself and rotate the credential.
Sign-out and revocation
Section titled “Sign-out and revocation”/logout ends this chat’s access at once and asks the API to delete the working token /login minted for you. The next message you send asks you to sign in again, and every browser session the login page issued for that chat identity ends with it. If the deletion fails, the bot says so and names the token, and you remove it yourself with hoody auth tokens delete <id>.
It then prints the id of the minting token and the single command that removes it:
# Run this yourself, from the CLI or the web UI. Nothing else can do it.hoody auth tokens delete "$TOKEN_ID"That command is yours alone to run. The minting token is a root token of your account, so no API token can delete it: not the kit’s, and not the working token it minted. Until you run it, that token stays valid until its 30-day expiry.
/logout is not the account-wide logout, and it never resolves to it. It ends this chat’s access and leaves your other sessions alone.
From the CLI, whoever operates the container can revoke one user’s lineage or every one of them:
# One chat user, by registration id and channel user id.hoody bot sessions revoke "$REGISTRATION" "$CHANNEL_USER"
# Every chat user of that registration.hoody bot tokens revoke "$REGISTRATION" --allBoth take the registration id as a positional argument, and both report the minting token ids they could not delete, so each account holder can run hoody auth tokens delete on their own. The revoked user’s next message answers with a prompt to sign in.
Operate the registration
Section titled “Operate the registration”The rest of the hoody bot group administers a running registration. Each command that administers one takes its id as the first positional argument; keys rotate is kit-wide and takes none.
# Publish the command list and menu button to Telegram, then read them back.hoody bot commands sync "$REGISTRATION"
# Bot name, descriptions and default administrator rights.hoody bot profile update "$REGISTRATION" --name "Ops" --short-description "Container control"
# The redacted audit log, newest first.hoody bot logs list "$REGISTRATION" --limit 50 --actor "$CHANNEL_USER"
# Delete audit rows older than the 90-day retention boundary.hoody bot logs purge "$REGISTRATION"
# Re-encrypt every sealed column under a new kit key.hoody bot keys rotatelogs purge refuses a cutoff inside the 90-day retention window rather than trimming it quietly; --all waives the window when you have decided to. keys rotate refuses to run while a poller is active unless you pass --force.
What the bot is not
Section titled “What the bot is not”It is not a service account with powers of its own. There is no token that belongs to it, no permission set of its own, and no identity the platform recognizes. It cannot act while nobody is signed in, and it cannot reach anything your own token cannot reach. What belongs to it is the encrypted Telegram token that lets it receive your messages, and that is the whole of it.
It is you, in a chat window, with the same container in front of you that is there when you sit down at your desk.
What’s next
Section titled “What’s next”- Bot: the kit itself, its commands, and its HTTP surface
- Hoody Bot API: the kit’s HTTP endpoints, their admission classes and their errors
- Chat Access: the sign-in paths, what the tokens may do, and what the design does not cover
- Hoody Agent: the agent service, which the bot will drive in a later release
- Deploying Autonomous AI Agents: give an agent its own container and let it work unattended
- Private Workflows: where the data sits, and how token scope and container isolation bound it