Skip to content
Hoody.com

Every Hoody container runs hoody-terminal, a service that exposes the container’s shell over HTTP. Each terminal session has a URL: open it in a browser for a full interactive terminal, POST to it to execute commands, or share it to put several people in the same session. There are no SSH keys to manage and nothing to configure.

The same sessions drive GUI programs: type firefox & in a terminal paired with a display and the window appears at the matching display URL.


hoody-terminal provides shell control over HTTP:

  • Web terminal UI - A full browser terminal that replaces SSH for most use cases
  • GUI launch - Type firefox & and the program appears in a browser display, with nothing to configure
  • Command execution - Run any shell command via POST request and get stdout, stderr, and exit codes
  • Persistent sessions - Stateful terminals that remember working directory and environment
  • Multiplayer sessions - Multiple users typing in the same terminal simultaneously
  • System monitoring - Query CPU, memory, disk, processes, and ports via HTTP
  • WebSocket streaming - Real-time output for long-running commands
  • Screenshots - Capture terminal state as PNG, JPEG, or GIF

The API reference documents every parameter, response, and example; the summary below links each endpoint.

Command execution:

  • POST /api/v1/terminal/execute - Execute shell commands
    • Body params: command, id, timeout, wait, cwd (per-command working directory), env
    • Query params: terminal_id, cwd (initial working directory for new/reset local sessions), cwd_auto_create, shell, user, ssh_host, ssh_user, ssh_password, ssh_key, reset
    • Modes: Synchronous (wait: true) or Asynchronous (wait: false)
  • GET /api/v1/terminal/result/{command_id} - Poll async command result
    • Returns: terminal_id, command_id, command, status, stdout, stderr, stdout_truncated, stderr_truncated, exit_code, timed_out, cancelled
  • POST /api/v1/terminal/execute/{command_id}/abort - Abort a running command (SIGINT or force SIGKILL)
    • Body: { "force": false }
  • POST /api/v1/terminal/write - Type raw input into a session PTY (interactive prompts, y/n, sudo password)
    • Query params: terminal_id
    • Body: { "input": "text", "enter": true }, written raw as if typed at the keyboard

Session management:

Terminal automation (TUI control):

System resource monitoring:

System control:

System introspection:

  • GET /api/v1/system/ports - List listening ports
    • Query params: protocol, user, port, ip, skip_program, http_only, hoody_only
  • GET /api/v1/system/daemon - List hoody-daemon programs
    • Returns: hoody-daemon-managed services with name, command, enabled, boot, autorestart
  • GET /api/v1/system/displays - List X11 displays
    • Returns: Connected displays with name, connected, resolution, scale, is_internal

WebSocket:

Health:

Web interface:


This is how most users access containers, and it replaces SSH for daily work:

https://{project}-{container}-terminal-1.{server}.containers.hoody.com
https://{project}-{container}-terminal-2.{server}.containers.hoody.com
https://{project}-{container}-terminal-3.{server}.containers.hoody.com

Each number is a separate terminal session in the same container:

  • terminal-1 - Your main terminal session
  • terminal-2 - A second terminal for monitoring logs
  • terminal-3 - A third terminal for running tests

All of them run in one container. Switch between them by opening different URLs; each keeps its own state.

Open a terminal URL in any browser, on a phone, tablet, laptop, or TV, and you get a full Linux terminal without an SSH client or any configuration.

The number in the URL (terminal-1, terminal-2) is the terminal ID. Switching terminals doesn’t change containers; you’re opening another shell session on the same computer.

Work persists across all terminals:

  • Files you create in terminal-1 are visible in terminal-2
  • Services you start in terminal-2 are accessible from terminal-3
  • Environment on the container remains consistent
  • The container is one computer with multiple shell sessions

To pair a terminal with a display, set the display field when the session is created; the kit then exports DISPLAY=:N into that shell. The common convention is to match the numbers:

  • terminal-1 with display: "1"DISPLAY=:1
  • terminal-2 with display: "2"DISPLAY=:2
  • terminal-5 with display: "5"DISPLAY=:5

With that pairing in place, GUI programs you launch in terminal-5 appear in display-5. There is no automatic terminal_id ⇒ DISPLAY mapping: pass the display field explicitly (or export DISPLAY=:3 inside the shell) to target a given display. This lets you organize applications across displays while controlling them from any terminal.

Five shells are pre-installed: bash (the default), zsh, fish, tmux, and sh.

ShellDescriptionLaunch
bashDefault shell, universal compatibilityDefault or ?shell=bash
zshModern shell, oh-my-zsh compatible, better completion?shell=zsh or exec zsh
fishFriendly shell, syntax highlighting, autosuggestions?shell=fish or exec fish
tmuxTerminal multiplexer, shared between web and SSH?shell=tmux
shBourne shell, minimal, POSIX-compliant?shell=sh

Switch shells from inside a session:

Terminal window
exec zsh # Switch to zsh
exec fish # Switch to fish
tmux # Launch tmux

When switching shells with the ?shell= parameter on an existing session, add ?reset=true so the new shell starts clean:

# Recommended: Reset when changing shells
https://PROJECT-CONTAINER-terminal-1.SERVER.containers.hoody.com/?shell=zsh&reset=true
# This ensures the new shell starts fresh without inheriting state from the previous shell

URL parameters customize the terminal:

?shell=zsh # Shell choice
&fontSize=14 # Larger text
&readonly=true # View-only mode
&title=Production%20Logs # Custom title
&panel=https://docs.hoody.com&panel-width=40% # Side panel with docs

See Web Terminal UI → for the 39 customization options.

Web terminal sessions are also reachable from SSH. tmux sessions are shared between web and SSH access, and the terminal number in the URL maps to the tmux session ID:

Terminal window
# Web terminal-3: https://{project}-{container}-terminal-3.{server}.containers.hoody.com
# SSH to same session:
ssh user@container
tmux attach -t 3
# Now you're in the exact same session as the web terminal
# Edit files, see command history; everything synced

This lets you drive web terminal sessions from traditional SSH clients while keeping full session state.

Scripts and AI agents use the HTTP API directly:

Terminal window
# Execute a command in your container
hoody terminal sessions exec --command "ls -la /app" --wait
# Run a long command with a timeout (still waits for completion; --wait is on by default)
hoody terminal sessions exec --command "npm run build" --timeout 300

Response: wait decides the shape of the response, and the two shapes are different.

With wait: true (the default) the request is held open until the command finishes, and the output comes back in that same response. There is nothing to poll:

{
"terminal_id": "1",
"command_id": "42",
"stdout": "total 48\ndrwxr-xr-x 5 user user 4096 Nov 9 14:30 .\n...",
"stderr": "",
"exit_code": 0,
"timed_out": false
}

With wait: false the call returns as soon as the command starts, and carries no output:

{
"terminal_id": "1",
"command_id": "42",
"status": "running",
"message": "Command started successfully"
}

Only then do you poll GET /api/v1/terminal/result/\{command_id\}, which also returns partial output while the command is still running:

// GET /api/v1/terminal/result/42
{
"terminal_id": "1",
"command_id": "42",
"command": "ls -la /app",
"status": "completed",
"stdout": "total 48\ndrwxr-xr-x 5 user user 4096 Nov 9 14:30 .\n...",
"stderr": "",
"exit_code": 0,
"timed_out": false
}

Prefer wait: false for anything long-running: each wait: true call occupies one of the service’s worker slots for the whole duration of the command, and the pool is small (4 by default).

The whole shell is now reachable by:

  • AI agents (execute commands via HTTP)
  • Mobile devices (POST from your phone)
  • Other containers (cross-container orchestration)
  • Embedded iframes (terminals in documentation)
  • Automation scripts (no SSH setup needed)

The terminal number in the URL determines which session executes the command:

  • terminal-1.hoody.com/execute → Executes in terminal session 1
  • terminal-2.hoody.com/execute → Executes in terminal session 2
  • Each session is isolated but in the same container

Sessions persist across requests:

// Execute in terminal-1
const terminalUrl = 'https://PROJECT_ID-CONTAINER_ID-terminal-1.SERVER_NAME.containers.hoody.com';
await fetch(terminalUrl + '/api/v1/terminal/execute', {
method: 'POST',
body: JSON.stringify({ command: 'cd /app', wait: true })
});
// Later, same terminal-1 - still in /app directory
await fetch(terminalUrl + '/api/v1/terminal/execute', {
method: 'POST',
body: JSON.stringify({ command: 'ls', wait: true })
});
// Lists contents of /app (working directory preserved)

Each terminal URL (terminal-1, terminal-2, terminal-3) is a distinct shell session within the same container:

  • Files created in terminal-1 are immediately accessible in terminal-2
  • Processes started in terminal-2 can be seen from terminal-3
  • All terminals share the same filesystem, users, and services; it is one computer

State persists within each terminal session:

  • Current working directory (cd commands remembered)
  • Environment variables (exports remain)
  • Shell history
  • Background processes
  • Open file descriptors

Work persists across the container: files, services, and databases remain regardless of which terminal you use.

The cwd parameter runs a command in a specific directory without a cd first:

// Execute in /var/log without changing session's working directory
await fetch(terminalUrl + '/api/v1/terminal/execute', {
method: 'POST',
body: JSON.stringify({
command: 'tail -f app.log',
cwd: '/var/log',
wait: false
})
});
// Session's working directory unchanged; next command runs in original location
await fetch(terminalUrl + '/api/v1/terminal/execute', {
method: 'POST',
body: JSON.stringify({ command: 'pwd' }) // Still in /home/user
});

Practical uses:

  • One-off commands in specific directories - Check logs, run tests, inspect files without navigating
  • Parallel operations - Different terminals in different directories simultaneously
  • CI/CD workflows - Execute build commands in consistent locations regardless of session state
  • Scripts - Always run from the correct directory without cd chains
// Build frontend and backend simultaneously in different directories
const build = async () => {
// Terminal-1: Frontend build
fetch(terminal1Url + '/api/v1/terminal/execute', {
method: 'POST',
body: JSON.stringify({
command: 'npm run build',
cwd: '/app/frontend',
wait: false
})
});
// Terminal-2: Backend build (same time)
fetch(terminal2Url + '/api/v1/terminal/execute', {
method: 'POST',
body: JSON.stringify({
command: 'go build',
cwd: '/app/backend',
wait: false
})
});
};

cwd overrides the directory for that one command only; the session’s working directory is untouched. If the terminal is already running with a different working directory, cwd still applies to just that single command.

To guarantee a specific starting directory, use ?reset=true:

# Guarantee terminal starts in /app directory
https://PROJECT-CONTAINER-terminal-1.hoody.com/?reset=true
# Then use cwd parameter to execute in specific locations
POST /api/v1/terminal/execute
{
"command": "npm test",
"cwd": "/app/frontend" // Executes in /app/frontend, session stays in /home/user
}

Reset and cwd together:

// Reset terminal to clean state
await fetch(terminalUrl + '?reset=true');
// Now session is in /home/user
// Use cwd for specific directory execution without changing session
await fetch(terminalUrl + '/api/v1/terminal/execute', {
method: 'POST',
body: JSON.stringify({
command: 'npm run build',
cwd: '/app/dist' // Runs in /app/dist, session remains in /home/user
})
});

?reset=true restarts the shell session from scratch:

https://PROJECT-CONTAINER-terminal-1.hoody.com/?reset=true

A reset:

  • Kills all processes in the session
  • Clears environment variables
  • Resets working directory to /home/user
  • Clears shell history
  • Re-executes .bashrc / .zshrc
  • Removes temporary state and caches

Typical reasons to reset:

// After testing that polluted the environment
// URL: ?reset=true
// Result: Clean environment, no leaked variables
// After failed deployment left processes running
// URL: ?reset=true
// Result: All processes killed, fresh start
// After experimenting with system configurations
// URL: ?reset=true
// Result: Back to default state
// Switching between different project contexts
// URL: ?reset=true
// Result: No leftover environment from previous project

Two ways to trigger it:

// Option 1: Reset via URL parameter (web terminal)
window.location = terminalUrl + '?reset=true';
// Option 2: Reset via DELETE endpoint (API)
await fetch(terminalUrl + '/api/v1/terminal/1', {
method: 'DELETE' // Kills session, next request creates fresh one
});
// Next command executes in brand new session
await fetch(terminalUrl + '/api/v1/terminal/execute', {
method: 'POST',
body: JSON.stringify({ command: 'env' }) // Clean environment
});

Reset versus delete:

  • ?reset=true - Immediate clean start; the web terminal reconnects automatically
  • DELETE /terminal/{id} - Kills the session; the next request creates a fresh one
  • Both give you a fresh shell; choose based on whether you’re using the web UI or the API

Common scenarios:

  • Development cycles - Reset between test runs to avoid state contamination
  • User demos - Start each demo with a clean environment
  • CI/CD stages - Reset before each deployment step
  • Troubleshooting - Eliminate environment issues by starting fresh

Share a terminal URL and everyone connected types in the same session. Multiple users connect to:

https://{project}-{container}-terminal-1.{server}.containers.hoody.com

Each connected client:

  • Sees the same terminal output (the PTY broadcasts to every attached client)
  • Can type commands simultaneously (all share one shell/screen)
  • Can attach read-only with ?readonly=true (input is blocked for that client only; read-only viewers and read-write collaborators can share one session)

This suits:

  • Pair programming (both typing in the same session)
  • Teaching Linux (instructor and students share a terminal)
  • Customer support (solve issues together)
  • Team debugging (everyone sees live output)

See Multiplayer by Default → for the design rationale.

Start a GUI program in a paired terminal and it appears in your browser:

Terminal window
# In a terminal paired with display 2 (created with display: "2"), run Firefox
firefox &
# It appears in display-2
# Open: https://{project}-{container}-display-2.{server}.containers.hoody.com

Pair the session with a display, type the command in the terminal URL, and the GUI appears at the matching display URL. There is nothing else to set up.

This works with any GUI program:

Terminal window
# In terminal-3
code /app # VS Code opens in display-3
libreoffice report.pdf # LibreOffice opens in display-3
gimp photo.jpg # GIMP opens in display-3
chrome # Chrome opens in display-3

For comparison, a traditional remote desktop means configuring a VNC or RDP server, installing a client, connecting to the desktop, and opening a terminal before the program finally runs. Here, typing the command in the terminal URL is the whole procedure.

The two URLs can sit on different devices:

  • Terminal URL on a phone → type code .
  • Display URL on a tablet → watch VS Code run
  • Control from anywhere, view from anywhere

Common uses:

  • Quick GUI app testing in the browser
  • Mobile development workflows (command on phone, view on tablet)
  • Teaching (instructor types, students see the GUI appear)
  • Documentation (embed a terminal and display showing a live app)

See Displays → for the full desktop experience.

The first time a terminal/desktop session for display N is created (CLI, SDK call, or URL), Hoody Terminal boots an X server on :N and attaches the dunst notification daemon to it. That is why launching a GUI from a paired terminal needs no setup, and why notifications sent to display N via the Notifications kit start dispatching as soon as the session exists.

hoody-terminal doubles as an SSH client: the container makes the SSH connection to the remote server, and you drive it over HTTP from a browser. There is no SSH client, key setup, or configuration on your device.

A traditional SSH workflow:

  1. Install SSH client on your device
  2. Generate SSH keys
  3. Copy keys to server
  4. Remember server addresses and ports
  5. Use command line to connect

The Hoody workflow:

  1. Open terminal URL in browser
  2. Add SSH parameters to URL
  3. You’re connected

Password authentication:

https://PROJECT-CONTAINER-terminal-1.hoody.com/
?ssh_host=production-server.com
&ssh_user=admin
&ssh_password=your_password

SSH key authentication:

# Base64-encode the key first:
# SSH_KEY=$(base64 -w0 /home/user/.ssh/id_rsa | jq -sRr @uri)
https://PROJECT-CONTAINER-terminal-1.hoody.com/
?ssh_host=192.168.1.100
&ssh_user=root
&ssh_key=<base64-encoded-private-key-content>

Custom port:

# Base64-encode the key first:
# SSH_KEY=$(base64 -w0 /keys/deploy_key | jq -sRr @uri)
https://PROJECT-CONTAINER-terminal-1.hoody.com/
?ssh_host=server.example.com
&ssh_port=2222
&ssh_user=deploy
&ssh_key=<base64-encoded-private-key-content>

You’re now controlling the remote server over HTTP, from any device with a browser.

For automation, the same SSH parameters work on the API. The connection persists, so you can send commands repeatedly to the same remote server:

// Connect to production database server
const remoteSession = await fetch(terminalUrl + '/api/v1/terminal/execute', {
method: 'POST',
body: JSON.stringify({
command: 'systemctl status postgresql',
wait: true
}),
headers: {
'Content-Type': 'application/json'
}
}).then(r => r.text());

With the SSH parameters in the URL:

import { readFileSync } from 'fs';
// Base64-encode the private key before embedding in the URL
const sshKey = readFileSync('/secure/postgres.key', 'base64');
const terminalUrl = 'https://PROJECT-CONTAINER-terminal-5.hoody.com' +
'?ssh_host=db-server.internal' +
'&ssh_user=postgres' +
'&ssh_key=' + encodeURIComponent(sshKey);

Now every POST to this URL executes commands on db-server.internal:

// All of these execute on the remote server, not the Hoody container
await fetch(terminalUrl + '/api/v1/terminal/execute', {
method: 'POST',
body: JSON.stringify({ command: 'pg_dump production > backup.sql' })
});
await fetch(terminalUrl + '/api/v1/terminal/execute', {
method: 'POST',
body: JSON.stringify({ command: 'du -sh backup.sql' })
});
await fetch(terminalUrl + '/api/v1/terminal/execute', {
method: 'POST',
body: JSON.stringify({ command: 'gzip backup.sql' })
});

The SSH connection stays open, and session state persists across requests.

Managing servers from a phone or tablet takes no SSH app and no terminal emulator, only a browser and a URL.

You can also share server access without sharing credentials:

https://docs-server-terminal-1.hoody.com?ssh_host=docs.internal&ssh_user=readonly

Send this URL to your team. They can:

  • Execute commands on docs.internal
  • Access immediately (no credential setup)
  • View output in the browser
  • Never see the actual SSH credentials

Access control comes from proxy permissions:

  • IP whitelist for production servers
  • Password protection for staging access
  • JWT tokens for automated deployments
  • See Permissions →

Monitoring production logs from a phone:

# base64 -w0 /keys/aws-prod.pem | jq -sRr @uri → use that value for ssh_key
https://PROJECT-CONTAINER-terminal-monitor.hoody.com/
?ssh_host=prod-app-1.aws.com
&ssh_user=ubuntu
&ssh_key=<base64-encoded-private-key-content>
&cmd=dGFpbCAtZiAvdmFyL2xvZy9hcHAubG9n # base64 of: tail -f /var/log/app.log

Bookmark the URL and open it on a phone to see live production logs without an SSH client.

A multi-server deployment script:

const servers = [
'web-1.production.com',
'web-2.production.com',
'web-3.production.com'
];
// Base64-encode deploy key once before the loop
import { readFileSync } from 'fs';
const deployKey = encodeURIComponent(readFileSync('/secure/deploy.key', 'base64'));
// Deploy to all servers via HTTP
for (const server of servers) {
const terminalUrl = `https://PROJECT-CONTAINER-terminal-deploy.hoody.com` +
`?ssh_host=${server}` +
`&ssh_user=deploy` +
`&ssh_key=${deployKey}`;
await fetch(terminalUrl + '/api/v1/terminal/execute', {
method: 'POST',
body: JSON.stringify({
command: 'cd /app && git pull && systemctl restart app',
wait: true,
timeout: 300
})
});
console.log(`Deployed to ${server}`);
}

Database backup automation:

import { readFileSync } from 'fs';
// Runs on Hoody container, connects to DB server via SSH, executes backup
const pgKey = encodeURIComponent(readFileSync('/keys/postgres.key', 'base64'));
const backupUrl = 'https://PROJECT-CONTAINER-terminal-backup.hoody.com' +
'?ssh_host=db-primary.internal' +
'&ssh_user=postgres' +
'&ssh_key=' + pgKey;
// This command runs on db-primary.internal, not Hoody container
await fetch(backupUrl + '/api/v1/terminal/execute', {
method: 'POST',
body: JSON.stringify({
command: `
pg_dump production | gzip > /backups/prod_$(date +%Y%m%d).sql.gz &&
aws s3 cp /backups/prod_$(date +%Y%m%d).sql.gz s3://backups/ &&
echo "Backup complete"
`,
wait: true
})
});

A security audit from a phone:

# base64 -w0 /keys/security-audit.key | jq -sRr @uri → use that value for ssh_key
https://PROJECT-CONTAINER-terminal-audit.hoody.com/
?ssh_host=firewall.company.com
&ssh_user=security
&ssh_key=<base64-encoded-private-key-content>
&cmd=aXB0YWJsZXMgLUwgLW4gLXY= # base64 of: iptables -L -n -v

Open the URL to see the firewall rules from any browser; no laptop or SSH client is involved.

Connections stay up until you reset:

import { readFileSync } from 'fs';
// ssh_key must be base64-encoded private key content, not a file path
const sshKeyB64 = readFileSync('/path/to/key', 'base64');
// Build the execute URL with SSH params as query string
const execUrl = terminalUrl + '/api/v1/terminal/execute?ssh_host=server.com&ssh_user=admin&ssh_key=' + encodeURIComponent(sshKeyB64);
// First command: Connects via SSH
await fetch(execUrl, { method: 'POST', body: JSON.stringify({ command: 'pwd' }) });
// Subsequent commands: Reuses connection (fast)
await fetch(execUrl, { method: 'POST', body: JSON.stringify({ command: 'ls' }) });
await fetch(execUrl, { method: 'POST', body: JSON.stringify({ command: 'df -h' }) });
// Connection stays open across requests; no reconnection overhead

Reset to change servers:

// Switch to different server
window.location = terminalUrl +
'?reset=true' + // Closes previous SSH connection
'&ssh_host=new-server.com' +
'&ssh_user=deploy' +
// ssh_key must be base64-encoded private key content, not a file path
'&ssh_key=' + encodeURIComponent(readFileSync('/keys/deploy.key', 'base64'));

Key points:

  • SSH connections are maintained by the Hoody container
  • Your device only speaks HTTP (browser or fetch)
  • Sessions persist, so repeated commands are fast
  • Credentials are stored in the container (keys in /keys/), not on the connecting device
  • Access control comes from proxy permissions, not SSH keys
  • Any device with a browser works

Together this means a phone can control production servers, run database backups, monitor logs, and deploy code, all via HTTP and without an SSH client.

A terminal and an AI agent can share one browser window. hoody-terminal embeds hoody-agent in a side panel: an assistant that can see your terminal, suggest commands, and help you work.

Add the agent URL as a side panel:

https://PROJECT-CONTAINER-terminal-1.hoody.com/
?panel=https://PROJECT-CONTAINER-agent-1.hoody.com
&panel-width=40%

What you get:

  • Left 40% - Hoody Agent (chat interface, suggestions, explanations)
  • Right 60% - Terminal (command execution)
  • Same container - Agent and terminal share filesystem, processes, and state

A typical loop:

  1. Ask the agent: “How do I find large files?”
  2. The agent responds: du -sh * | sort -h
  3. Execute the command in the terminal (right side)
  4. The agent sees the result and offers a next step
  5. Repeat

Each terminal can have its own dedicated agent:

# Terminal-1 with agent-1 (configured for frontend context)
https://PROJECT-CONTAINER-terminal-1.hoody.com/
?panel=https://PROJECT-CONTAINER-agent-1.hoody.com
&panel-width=35%
# Terminal-2 with agent-2 (configured for backend context)
https://PROJECT-CONTAINER-terminal-2.hoody.com/
?panel=https://PROJECT-CONTAINER-agent-2.hoody.com
&panel-width=35%
# Terminal-3 with agent-3 (configured for DevOps context)
https://PROJECT-CONTAINER-terminal-3.hoody.com/
?panel=https://PROJECT-CONTAINER-agent-3.hoody.com
&panel-width=35%

Each agent instance:

  • Is configured for a specific context (via profiles, memory, system prompt)
  • Has access to the terminal history in its pane
  • Can see files and processes in the container
  • Provides specialized assistance based on its configuration

Bookmark these URLs to reopen a configured terminal-and-agent pair later.

Learning Linux and DevOps:

https://PROJECT-CONTAINER-terminal-1.hoody.com/
?panel=https://PROJECT-CONTAINER-agent-1.hoody.com
&panel-width=40%
  • Ask the agent to explain commands before running them
  • The agent provides context, flags, and common patterns
  • Execute in the terminal, see results
  • The agent explains output and suggests next steps

Debugging:

https://PROJECT-CONTAINER-terminal-2.hoody.com/
?panel=https://PROJECT-CONTAINER-agent-2.hoody.com
&panel-width=35%
  • Describe the problem to the agent: “API returning 500 errors”
  • The agent suggests diagnostic commands: tail -f /var/log/app.log
  • Execute the commands in the terminal
  • The agent analyzes logs and suggests fixes
  • Apply fixes; the agent monitors results

Infrastructure management:

https://PROJECT-CONTAINER-terminal-3.hoody.com/
?panel=https://PROJECT-CONTAINER-agent-3.hoody.com
&panel-width=40%
&ssh_host=production-server.com
&ssh_user=admin
  • Combined SSH, agent, and terminal
  • SSH to the production server (via HTTP)
  • The agent helps with server management commands
  • Safe infrastructure changes with AI assistance

Code review and testing:

https://PROJECT-CONTAINER-terminal-4.hoody.com/
?panel=https://PROJECT-CONTAINER-agent-4.hoody.com
&panel-width=45%
  • The agent reviews code in /app/src
  • It suggests improvements and test commands
  • Execute tests in the terminal: npm test
  • The agent analyzes test output and suggests fixes
  • Iterate until tests pass

From the panel the agent can:

  • Read files in the container
  • Execute commands (if you grant permission)
  • Analyze output from the terminal
  • Suggest solutions based on context
  • Remember the conversation across page reloads (via agent state)
  • Access container services (databases, web servers, and so on)

Panel width is adjustable:

&panel-width=30% # Smaller panel, more terminal space
&panel-width=50% # Equal split
&panel-width=60% # Larger panel for complex agent responses

Pick a width to match the work: debugging needs more terminal space, learning needs more agent space.

Larger projects can run several agents:

Terminal window
# Frontend terminal with agent-1 (configured for React/TypeScript)
open "https://PROJECT-CONTAINER-terminal-1.hoody.com/?panel=https://PROJECT-CONTAINER-agent-1.hoody.com"
# Backend terminal with agent-2 (configured for Go/databases)
open "https://PROJECT-CONTAINER-terminal-2.hoody.com/?panel=https://PROJECT-CONTAINER-agent-2.hoody.com"
# Database terminal with agent-3 (configured for PostgreSQL)
open "https://PROJECT-CONTAINER-terminal-3.hoody.com/?panel=https://PROJECT-CONTAINER-agent-3.hoody.com&ssh_host=db.internal"

Each agent instance is configured differently:

  • agent-1: React/TypeScript context (via profiles and memory)
  • agent-2: Go/API context (via profiles and memory)
  • agent-3: PostgreSQL/DBA context (via profiles and memory)

All of them work in the same container: files created in one terminal are visible to the others, and services are reachable across terminals, which suits full-stack development.

A traditional setup:

  1. Open terminal application
  2. Open separate AI chat in browser
  3. Switch between them
  4. Copy/paste commands and output
  5. Context lost between apps

The Hoody setup:

  1. Open one URL (terminal + agent)
  2. Agent sees what you type
  3. Agent sees command output
  4. Suggest, execute, analyze in one window
  5. Context preserved automatically

The URL carries the whole environment: share it and you share the setup, including the terminal, the agent, the SSH connection, and the panel layout.

This suits:

  • Teaching - The instructor shares a terminal-and-agent URL; students learn with AI assistance
  • Pair programming - Both developers see the terminal and the agent’s suggestions
  • Support - Share a debugging session with the AI already analyzing the problem
  • Documentation - Embed a terminal and agent showing live examples

See Web Terminal UI → for all panel customization options.

Query the container’s state via HTTP:

Terminal window
# Get CPU/memory/disk stats
hoody terminal system resources
# List running processes sorted by CPU
hoody terminal processes list --sort cpu --limit 10
# Check what ports are listening
hoody terminal system ports
# View X11 displays
hoody terminal system display-info

All of this system information is served over HTTP endpoints; there is no need to SSH in and run commands.

Execute commands on other servers through hoody-terminal:

Terminal window
hoody terminal sessions exec \
--command "systemctl status nginx" --wait \
--ssh-host prod.example.com \
--ssh-user admin \
--ssh-password "$SSH_PASSWORD"

The container acts as an HTTP-to-SSH bridge: your phone can execute commands on your production servers via HTTP.


SSH Client (installed) → SSH Server (configured) → Shell (finally)

Limitations:

  • SSH client required (not available on phones, tablets, watches)
  • Key management (private keys, known_hosts, permissions)
  • Port forwarding complexity
  • Not embeddable (you can’t iframe SSH)
  • Not multiplayer (one session per connection)
  • AI can’t use it (binary protocol)
Any HTTP Client → hoody-terminal URL → Shell (immediately)

Advantages:

  • Any device with a browser works (phones, tablets, watches, TVs)
  • No key management (the URL is the credential)
  • Naturally embeddable (<iframe src="terminal-url" />)
  • Multiplayer by default (sharing the URL shares the session)
  • AI-native (LLMs understand HTTP requests)
  • Observable (all HTTP requests logged)
  • MITM-able via hoody-exec (enhance any command automatically)

Because terminals are HTTP:

  1. Embed in documentation

    <iframe src="https://demo-terminal.hoody.com/?cmd=bHMgLWxh&readonly=true" />

    Live terminals in your docs showing actual command execution.

  2. Phone executes production commands

    // From mobile browser
    await fetch(terminalUrl + '/api/v1/terminal/execute', {
    method: 'POST',
    body: JSON.stringify({
    command: 'pm2 restart api',
    wait: true
    })
    });
  3. AI orchestrates infrastructure

    // AI agent deploys automatically
    const steps = [
    'git pull origin main',
    'npm install --production',
    'npm run build',
    'pm2 restart app'
    ];
    for (const cmd of steps) {
    await fetch(terminalUrl + '/api/v1/terminal/execute?terminal_id=4', {
    method: 'POST',
    body: JSON.stringify({ command: cmd, wait: true })
    });
    }
  4. Cascading execution

    // Terminal A executes command that triggers Terminal B
    await fetch(containerA_terminal + '/api/v1/terminal/execute', {
    method: 'POST',
    body: JSON.stringify({
    command: `curl -X POST ${containerB_terminal}/api/v1/terminal/execute -d '{"command":"npm test"}'`,
    wait: true
    })
    });

Start the task asynchronously and check on it later:

// Use terminal-3 for build tasks
const buildTerminalUrl = 'https://PROJECT_ID-CONTAINER_ID-terminal-3.SERVER_NAME.containers.hoody.com';
const response = await fetch(buildTerminalUrl + '/api/v1/terminal/execute', {
method: 'POST',
body: JSON.stringify({
command: 'npm run build',
wait: false,
timeout: 300
})
});
const { command_id } = await response.json();
// Poll for result
const checkStatus = async () => {
const result = await fetch(buildTerminalUrl + `/api/v1/terminal/result/${command_id}`)
.then(r => r.json());
if (result.status === 'completed') {
console.log(`Build finished with exit code ${result.exit_code}`);
console.log(result.stdout);
} else {
setTimeout(checkStatus, 2000);
}
};
checkStatus();

To watch progress, open the web terminal URL in a browser: the same terminal-3 URL shows the build output live.

The two modes mix: start a command via the API (from a phone, script, or CI/CD), then follow it visually in the web terminal from any browser.

Multi-step workflows execute in sequence in the same session:

const deployCommands = [
'cd /app',
'git pull origin main',
'npm ci',
'npm run build',
'pm2 restart api',
'pm2 logs api --lines 50'
];
// Use terminal-2 for deployments
const deployUrl = 'https://PROJECT_ID-CONTAINER_ID-terminal-2.SERVER_NAME.containers.hoody.com';
for (const command of deployCommands) {
const response = await fetch(deployUrl + '/api/v1/terminal/execute', {
method: 'POST',
body: JSON.stringify({ command, wait: true })
});
const result = await response.json();
if (result.exit_code !== 0) {
console.error(`Failed at: ${command}`);
console.error(result.stderr);
break;
}
console.log(`${command}`);
}
// All commands executed in terminal-2's session
// Working directory persisted across commands (cd /app stayed active)

Monitor container resources:

async function checkHealth(terminalUrl) {
const response = await fetch(terminalUrl + '/api/v1/system/resources');
const { cpu, memory } = await response.json();
const alerts = [];
if (cpu.usage_percent > 80) {
alerts.push(`WARNING: CPU at ${cpu.usage_percent}%`);
}
if (memory.used_percent > 85) {
alerts.push(`WARNING: Memory at ${memory.used_percent}%`);
}
return alerts;
}
// Run every 5 minutes
setInterval(async () => {
const alerts = await checkHealth(terminalUrl);
if (alerts.length > 0) {
console.log('Health alerts:', alerts);
}
}, 300000);

Collaborative debugging: a team member encounters a bug. Instead of screen sharing:

  1. Share the terminal URL: https://{project}-{container}-terminal-2.{server}.containers.hoody.com
  2. A senior dev opens the URL on a phone while in a meeting
  3. Types the fix directly in the shared session
  4. The bug is resolved in 30 seconds

There is no screen-share setup and no “can you see this?”; both people are in the same shell.

AI agents execute while you orchestrate:

// AI agent executes commands via the terminal HTTP API
const TERMINAL_URL = 'https://PROJECT_ID-CONTAINER_ID-terminal-1.SERVER_NAME.containers.hoody.com';
const commands = [
'npm create vite@latest my-app -- --template react-ts',
'cd my-app && npm install',
'npm run dev'
];
for (const command of commands) {
const result = await fetch(`${TERMINAL_URL}/api/v1/terminal/execute`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command, wait: true })
});
console.log(await result.json());
}
// You described what you want built. AI executed it via HTTP.

Automation needs no SSH setup:

# Deploy from GitHub Actions, GitLab CI, any CI/CD
import requests
terminal_url = os.environ['HOODY_TERMINAL_URL']
response = requests.post(
f'{terminal_url}/api/v1/terminal/execute',
params={'terminal_id': '5'},
json={
'command': './deploy.sh production',
'wait': False,
'timeout': 600
}
)
command_id = response.json()['command_id']
# Monitor deployment status via command_id

Support agent and customer solve issues together in real time. Both open:

https://{project}-{customerContainer}-terminal-1.{server}.containers.hoody.com

Both see the same terminal and both can type. The agent fixes the issue while the customer watches, rather than dictating commands and hoping they are typed correctly.

Administer servers from anywhere. Your phone’s browser opens the terminal URL and executes:

  • systemctl restart nginx
  • tail -f /var/log/app.log
  • docker ps
  • htop

A full Linux shell on a phone, because the terminal is a URL.

Embed working terminals in docs:

<!-- Your documentation -->
<p>To check system status, run:</p>
<iframe
src="https://demo-terminal.hoody.com/?cmd=c3lzdGVtY3RsIHN0YXR1cyBuZ2lueA==&readonly=true"
height="400"
/>

Readers see actual execution, not just code blocks.


Section titled “Use the same terminal URL for related tasks”

Group related commands under the same terminal URL to keep session context:

// Good - Same terminal URL maintains state
const deployUrl = 'https://PROJECT_ID-CONTAINER_ID-terminal-2.SERVER_NAME.containers.hoody.com';
await fetch(deployUrl + '/api/v1/terminal/execute', {
method: 'POST',
body: JSON.stringify({ command: 'cd /app', wait: true })
});
await fetch(deployUrl + '/api/v1/terminal/execute', {
method: 'POST',
body: JSON.stringify({ command: 'npm install', wait: true })
});
// Runs in /app (working directory preserved)
// Bad - Different terminal URLs = different sessions
await fetch('...terminal-1.hoody.com/api/v1/terminal/execute', {
body: JSON.stringify({ command: 'cd /app' })
});
await fetch('...terminal-2.hoody.com/api/v1/terminal/execute', {
body: JSON.stringify({ command: 'npm install' })
});
// Runs in ~ (terminal-2 has its own separate session)

terminal-1, terminal-2, and terminal-3 are all in the same container: same files, same services, same system. Each just has an independent shell session with its own working directory and command history.

Use wait: false for long-running commands so the request returns immediately with a command_id to poll (wait defaults to true):

// Async for builds
await fetch('.../api/v1/terminal/execute', {
body: JSON.stringify({
command: 'npm run build',
wait: false,
timeout: 300
})
});
// Sync would block for minutes

Prevent runaway processes with sensible timeouts:

{
command: 'npm install',
timeout: 300, // 5 minutes max
wait: false
}

Don’t assume success; verify exit codes:

const result = await execute({ command: 'npm test', wait: true });
if (result.exit_code !== 0) {
console.error('Tests failed:', result.stderr);
// Handle failure
} else {
console.log('Tests passed!');
}

Delete sessions when done to prevent resource leaks:

Terminal window
# List active sessions
GET /api/v1/terminal/sessions
# Delete old sessions
DELETE /api/v1/terminal/{terminal_id}

For debugging or exploration, open the web URL directly:

https://{project}-{container}-terminal-1.{server}.containers.hoody.com

It beats the API for typing multiple commands, colored output, and scrolling through history.

Query the resource endpoints before intensive operations:

const { cpu, memory } = await fetch('.../system/resources')
.then(r => r.json());
if (cpu.usage_percent > 90) {
console.warn('CPU at capacity - delay operation');
}

Yes, if the terminal is running as root or the user has sudo permissions. Configure the user when creating the container or use the user parameter when starting the terminal session. For security, consider using separate containers for privileged and unprivileged operations.

How do I execute commands on remote servers via SSH?

Section titled “How do I execute commands on remote servers via SSH?”

Include SSH parameters in the execute request: ssh_host, ssh_user, ssh_password (or ssh_key). The hoody-terminal service establishes the SSH connection and executes the command, returning results via HTTP. Your container becomes an HTTP-to-SSH gateway.

Can AI agents really use terminals effectively?

Section titled “Can AI agents really use terminals effectively?”

Yes. LLMs understand HTTP natively and can construct command execution requests without special training. They can handle multi-step workflows, check exit codes, parse output, and adapt based on results, all via standard HTTP calls and without an SDK.

What happens to running commands if I close my browser?

Section titled “What happens to running commands if I close my browser?”

Commands continue executing on the server; terminal sessions are server-side, not browser-side. Close your laptop and the command keeps running. Check status later via the /result/{command_id} endpoint.

Yes. Use iframes to embed terminal URLs directly in your app. Common patterns: documentation with live command execution, dashboards showing server logs, customer portals with diagnostic terminals.

How many terminal sessions can one container have?

Section titled “How many terminal sessions can one container have?”

There is no fixed limit; in practice, hundreds of concurrent sessions work, and each consumes minimal RAM. Use different terminal_id values for isolated sessions or the same terminal_id for shared/multiplayer sessions.

Does the web terminal work on mobile devices?

Section titled “Does the web terminal work on mobile devices?”

Yes, in the native browsers on iOS and Android. The UI adapts to touch input, includes an on-screen keyboard option, and supports mobile gestures, so you get a full Linux terminal from a phone’s browser.

Can I capture screenshots of terminal sessions?

Section titled “Can I capture screenshots of terminal sessions?”

Yes, via GET /api/v1/terminal/screenshot?terminal_id=1&format=png. It returns a visual snapshot as PNG, JPEG, or GIF, which is useful for documentation, tutorials, or monitoring dashboards showing live terminal state.

How do I get the complete command history for a session?

Section titled “How do I get the complete command history for a session?”

Use GET /api/v1/terminal/history/{terminal_id} to retrieve all commands executed, their exit codes, and execution times. Useful for auditing, debugging, or working out what changed in a session.


Check that the container is running:

Terminal window
curl "https://api.hoody.com/api/v1/containers/{id}?runtime=true" \
-H "Authorization: Bearer $HOODY_TOKEN"

Verify status: "running" and that runtime_info.terminals shows active sessions.

Start it if it is stopped:

Terminal window
curl -X POST "https://api.hoody.com/api/v1/containers/{id}/start" \
-H "Authorization: Bearer $HOODY_TOKEN"

Possible causes:

  1. Container not running - Start it via the Hoody API
  2. Wrong URL - Verify terminal-1 (not terminal1 or terminal)
  3. Proxy permissions - Check whether terminal access is restricted
  4. Browser blocking iframe - Some browsers block cross-origin iframes

Quick test:

Terminal window
# Direct browser access (not iframe)
https://{project}-{container}-terminal-1.{server}.containers.hoody.com

For long-running commands, use async mode:

// Correct - Async for npm install
{
command: 'npm install',
wait: false,
timeout: 300
}
// Wrong - Sync will timeout
{
command: 'npm install',
wait: true // Blocks for minutes
}

Increase the timeout if needed:

{ timeout: 600 } // 10 minutes for large builds

Check stderr, not just exit_code:

const result = await execute({ command: 'npm test', wait: true });
// Some commands exit 0 but write errors to stderr
if (result.stderr.includes('ERROR') || result.stderr.includes('FAIL')) {
console.error('Command had errors:', result.stderr);
}

Make sure you use the same terminal_id:

// Correct - Same session (terminal_id must be numeric 1-65535)
terminal_id: "42" // State persists
// Wrong - Different sessions
terminal_id: Math.random() // New session each time

Sessions are deleted on:

  • Explicit DELETE request
  • Container restart
  • Terminal service restart

Check the SSH parameters:

// SSH parameters are query params on the execute URL, not the JSON body.
// POST /api/v1/terminal/execute?ssh_host=...&ssh_port=22&ssh_user=...
const execUrl = terminalUrl +
'/api/v1/terminal/execute' +
'?ssh_host=prod.example.com' +
'&ssh_port=22' + // Default 22 if omitted
'&ssh_user=admin' +
'&ssh_password=your-password'; // Or ssh_key (base64-encoded key content)
// The body carries only the command:
await fetch(execUrl, { method: 'POST', body: JSON.stringify({ command: 'ls' }) });

Verify SSH connectivity:

Terminal window
# Test from container first
ssh admin@prod.example.com

Common issues:

  • Firewall blocking the SSH port
  • Wrong credentials
  • Host or network unreachable from the container (host key verification is not an issue: the kit connects with StrictHostKeyChecking=no, so unknown hosts are accepted automatically)

Other interactive services:

Displays

Full desktop environments accessible via URL: run VS Code, browsers, any GUI application.

Explore Displays →

Browser

Chrome automation as a REST API: control browsers via HTTP, scrape websites, run tests.

Explore Browser →

Exec

Turn any script into an HTTP endpoint; your code becomes an API automatically.

Explore Exec →

Terminal API reference: