Displays
Full desktop environments accessible via URL—run VS Code, browsers, any GUI application.
Your shell is a URL. Execute commands via HTTP, share terminal sessions with a link, collaborate in real-time—no SSH keys, no configuration, just pure HTTP.
Launch GUI applications instantly: Type firefox & in the terminal and it appears in your browser via the matching display URL. No setup. No configuration. Just type and see.
Every Hoody container includes hoody-terminal, transforming your Linux shell into a first-class web service accessible via HTTP endpoints.
hoody-terminal provides complete shell control through HTTP:
firefox & and it appears in browser—zero configurationstdout/stderr/exit codesOfficial Technical Reference:
For complete endpoint documentation with all parameters, responses, and examples:
Command Execution:
command, id, timeout, wait, cwd (per-command working directory), envterminal_id, cwd (initial working directory for new/reset local sessions), cwd_auto_create, shell, user, ssh_host, ssh_user, ssh_password, ssh_key, resetwait: true) or Asynchronous (wait: false)status, exit_code, stdout, stderr, duration_ms{ "force": false }terminal_id{ "input": "text", "enter": true } — raw byte injection as if typed at a keyboardSession Management:
terminal_id, shell, user, cwd, display, ssh_host, …terminal_id, format (download|text|html)terminal_id, format (png|jpeg|gif), foreground, background, fontsizeTerminal Automation (TUI Control):
terminal_id, include_colors, include_highlights, scroll_offsetterminal_id, pattern, scope, limit, case_insensitive{ "key": "enter" } or { "keys": [...] }{ "text": "...", "bracketed": true }{ "mode": "stable|regex|either", "pattern": "...", "timeout_ms": 5000, "debounce_ms": 100 }/pressSystem Resource Monitoring:
sort (cpu|memory|pid), limit, filter (by name){"pid": 12345, "signal": "SIGTERM"} or {"name": "nginx", "signal": "SIGHUP"}System Control:
System Introspection:
http_only, hoody_only, user, portWebSocket:
Health:
Web Interface & Authentication:
This is how most users access containers—replaces SSH for daily work:
https://{project}-{container}-terminal-1.{server}.containers.hoody.icuhttps://{project}-{container}-terminal-2.{server}.containers.hoody.icuhttps://{project}-{container}-terminal-3.{server}.containers.hoody.icuEach number is a separate terminal session in the SAME container:
terminal-1 - Your main terminal sessionterminal-2 - A second terminal for monitoring logsterminal-3 - A third terminal for running testsAll in one container. Switch between them by opening different URLs. Each maintains its own state.
Open in ANY browser—phone, tablet, laptop, TV—and you have a full Linux terminal. No SSH client needed. No configuration. Just a URL.
Key insight: The number in the URL (terminal-1, terminal-2) IS the terminal ID. Switching terminals doesn’t change containers—you’re just opening another shell session in the same computer.
Your work persists across all terminals:
Display integration: Pair a terminal with a display by setting 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=:1terminal-2 with display: "2" → DISPLAY=:2terminal-5 with display: "5" → DISPLAY=:5With 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 from any terminal.
Shell selection—choose your preferred shell:
Pre-installed shells: 🐚 bash (default) • ⚡ zsh • 🐠 fish • 📋 tmux • 🐚 sh
| Shell | Description | Launch |
|---|---|---|
| 🐚 bash | Default shell, universal compatibility | Default or ?shell=bash |
| ⚡ zsh | Modern shell, oh-my-zsh compatible, better completion | ?shell=zsh or exec zsh |
| 🐠 fish | Friendly shell, syntax highlighting, autosuggestions | ?shell=fish or exec fish |
| 📋 tmux | Terminal multiplexer, shared between web and SSH | ?shell=tmux |
| 🐚 sh | Bourne shell, minimal, POSIX-compliant | ?shell=sh |
Quick shell switching:
exec zsh # Switch to zshexec fish # Switch to fishtmux # Launch tmuxChanging shells via URL: When switching shells using the ?shell= parameter on an existing session, use ?reset=true to ensure a clean start:
# Recommended: Reset when changing shellshttps://PROJECT-CONTAINER-terminal-1.hoody.icu/?shell=zsh&reset=true
# This ensures the new shell starts fresh without inheriting state from the previous shellCustomize via URL parameters:
?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 docsSee: Web Terminal UI → for 39 customization options.
Access web terminal sessions from SSH →:
tmux sessions are shared between web and SSH access. The terminal number in the URL maps to tmux session ID:
# Web terminal-3: https://{project}-{container}-terminal-3.{server}.containers.hoody.icu# SSH to same session:ssh user@containertmux attach -t 3
# Now you're in the exact same session as the web terminal# Edit files, see command history—everything syncedThis lets you access web terminal sessions from traditional SSH clients while maintaining full session state.
For scripts and AI agents, use the HTTP API directly:
# Execute a command in your containerhoody terminal sessions exec --command "ls -la /app" --wait
# Run a command asynchronouslyhoody terminal sessions exec --command "npm run build" --no-wait --timeout 300import { HoodyClient } from '@hoody-ai/hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });
// Execute a shell command (synchronous)const result = await containerClient.terminal.execution.execute( { command: 'ls -la /app', wait: true }, // request body { terminal_id: '1' } // query params — terminal_id matches the terminal-1 URL);console.log(result.data.stdout);curl -X POST "https://$PROJECT-$CONTAINER-terminal-1.$SERVER.containers.hoody.icu/api/v1/terminal/execute" \ -H "Content-Type: application/json" \ -d '{"command": "ls -la /app", "wait": true}' Response:
{ "success": true, "command_id": "cmd-123", "terminal_id": "1", "status": "completed", "exit_code": 0, "stdout": "total 48\ndrwxr-xr-x 5 user user 4096 Nov 9 14:30 .\n...", "stderr": "", "duration_ms": 5}The breakthrough: Your entire shell is now accessible to:
The URL terminal number determines which session executes the command:
terminal-1.hoody.icu/execute → Executes in terminal session 1terminal-2.hoody.icu/execute → Executes in terminal session 2Sessions persist across requests:
// Execute in terminal-1const terminalUrl = 'https://PROJECT_ID-CONTAINER_ID-terminal-1.SERVER_NAME.containers.hoody.icu';
await fetch(terminalUrl + '/api/v1/terminal/execute', { method: 'POST', body: JSON.stringify({ command: 'cd /app', wait: true })});
// Later, same terminal-1 - still in /app directoryawait fetch(terminalUrl + '/api/v1/terminal/execute', { method: 'POST', body: JSON.stringify({ command: 'ls', wait: true })});// Lists contents of /app (working directory preserved)Critical understanding: Each terminal URL (terminal-1, terminal-2, terminal-3) represents a distinct shell session within the SAME container:
State persists within each terminal session:
cd commands remembered)Work persists across the container: Files, services, databases—everything remains regardless of which terminal you use.
cwdDon’t want to cd first? Use the cwd parameter to execute commands in any directory:
// Execute in /var/log without changing session's working directoryawait 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 locationawait fetch(terminalUrl + '/api/v1/terminal/execute', { method: 'POST', body: JSON.stringify({ command: 'pwd' }) // Still in /home/user});Practical uses:
cd chains// Build frontend and backend simultaneously in different directoriesconst build = async () => { // Terminal-1: Frontend build fetch(terminal1Url + '/execute', { method: 'POST', body: JSON.stringify({ command: 'npm run build', cwd: '/app/frontend', wait: false }) });
// Terminal-2: Backend build (same time) fetch(terminal2Url + '/execute', { method: 'POST', body: JSON.stringify({ command: 'go build', cwd: '/app/backend', wait: false }) });};Key insight: cwd overrides directory temporarily for that ONE command only—session state remains untouched.
Important: The cwd parameter affects the command execution directory but does NOT change the session’s working directory. If the terminal is already running with a different working directory, cwd only applies to that single command.
For guaranteed directory control: Use ?reset=true when you need to ensure a specific starting directory:
# Guarantee terminal starts in /app directoryhttps://PROJECT-CONTAINER-terminal-1.hoody.icu/?reset=true
# Then use cwd parameter to execute in specific locationsPOST /api/v1/terminal/execute{ "command": "npm test", "cwd": "/app/frontend" // Executes in /app/frontend, session stays in /home/user}Reset + cwd workflow:
// Reset terminal to clean stateawait fetch(terminalUrl + '?reset=true');
// Now session is in /home/user// Use cwd for specific directory execution without changing sessionawait 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 })});resetNeed a clean slate? Use ?reset=true to restart the shell session:
https://PROJECT-CONTAINER-terminal-1.hoody.icu/?reset=trueWhat reset does:
/home/user.bashrc / .zshrc executionWhen to use 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 projectPractical workflow:
// 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 sessionawait fetch(terminalUrl + '/api/v1/terminal/execute', { method: 'POST', body: JSON.stringify({ command: 'env' }) // Clean environment});Reset vs Delete:
?reset=true - Immediate clean start, auto-reconnects web terminalDELETE /terminal/{id} - Kills session, requires new request to recreateCommon scenarios:
Share a terminal URL and everyone types together:
Multiple users connecting to:
https://{project}-{container}-terminal-1.{server}.containers.hoody.icuEach connected client:
?readonly=true (input blocked for that client only — mix read-only viewers with read-write collaborators in the same session)Perfect for:
See: Multiplayer by Default → for collaboration philosophy.
The killer feature: Start any GUI program and see it immediately in your browser.
# In a terminal paired with display 2 (created with display: "2"), run Firefoxfirefox &
# It appears in display-2# Open: https://{project}-{container}-display-2.{server}.containers.hoody.icuThat’s it. Pair the session with a display, type the command in the terminal URL, and the GUI appears in the matching display URL. No configuration. No setup. Instant graphical applications.
Works with ANY GUI program:
# In terminal-3code /app # VS Code opens in display-3libreoffice report.pdf # LibreOffice opens in display-3gimp photo.jpg # GIMP opens in display-3chrome # Chrome opens in display-3Why this is revolutionary:
Traditional remote desktop: Configure VNC/RDP server, install client, connect to desktop, open terminal, run program (finally).
Hoody: Type command in terminal URL. Program appears in display URL. Done.
Your phone can now run:
code .Perfect for:
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’s why the URL trick above just works — and it’s also why notifications sent to display N via the Notifications kit start dispatching as soon as the session exists.
The breakthrough: Manage ANY server from a browser—no SSH client, no keys, no configuration.
Hoody Terminal becomes your SSH client. Connect to remote servers via HTTP—the Hoody container makes the SSH connection FOR you.
Traditional SSH workflow:
Hoody workflow:
Password authentication:
https://PROJECT-CONTAINER-terminal-1.hoody.icu/ ?ssh_host=production-server.com &ssh_user=admin &ssh_password=your_passwordSSH 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.icu/ ?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.icu/ ?ssh_host=server.example.com:2222 &ssh_user=deploy &ssh_key=<base64-encoded-private-key-content>You’re now controlling the remote server—through HTTP—from any device with a browser.
The SSH connection persists—send commands repeatedly to the same remote server:
// Connect to production database serverconst 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());URL already has SSH params:
import { readFileSync } from 'fs';// Base64-encode the private key before embedding in the URLconst sshKey = readFileSync('/secure/postgres.key', 'base64');const terminalUrl = 'https://PROJECT-CONTAINER-terminal-5.hoody.icu' + '?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 these execute on the REMOTE server, not the Hoody containerawait 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—session state persists across requests.
Manage servers from phones/tablets:
Share server access without credentials:
https://docs-server-terminal-1.hoody.icu?ssh_host=docs.internal&ssh_user=readonlySend this URL to your team. They can:
docs.internalControl via proxy permissions:
Monitor production logs from phone:
# base64 -w0 /keys/aws-prod.pem | jq -sRr @uri → use that value for ssh_keyhttps://PROJECT-CONTAINER-terminal-monitor.hoody.icu/ ?ssh_host=prod-app-1.aws.com &ssh_user=ubuntu &ssh_key=<base64-encoded-private-key-content> &command=tail%20-f%20/var/log/app.logBookmark this URL. Open on phone. See live production logs—no SSH client needed.
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 loopimport { readFileSync } from 'fs';const deployKey = encodeURIComponent(readFileSync('/secure/deploy.key', 'base64'));
// Deploy to all servers via HTTPfor (const server of servers) { const terminalUrl = `https://PROJECT-CONTAINER-terminal-deploy.hoody.icu` + `?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 backupconst pgKey = encodeURIComponent(readFileSync('/keys/postgres.key', 'base64'));const backupUrl = 'https://PROJECT-CONTAINER-terminal-backup.hoody.icu' + '?ssh_host=db-primary.internal' + '&ssh_user=postgres' + '&ssh_key=' + pgKey;
// This command runs on db-primary.internal, not Hoody containerawait 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 })});Security audit from phone:
# base64 -w0 /keys/security-audit.key | jq -sRr @uri → use that value for ssh_keyhttps://PROJECT-CONTAINER-terminal-audit.hoody.icu/ ?ssh_host=firewall.company.com &ssh_user=security &ssh_key=<base64-encoded-private-key-content> &command=iptables%20-L%20-n%20-vOpen URL. See firewall rules. No laptop. No SSH client. Just HTTP.
Connections auto-maintain until you reset:
import { readFileSync } from 'fs';// ssh_key must be base64-encoded private key content, not a file pathconst sshKeyB64 = readFileSync('/path/to/key', 'base64');// Session created with SSH paramsconst url = terminalUrl + '?ssh_host=server.com&ssh_user=admin&ssh_key=' + encodeURIComponent(sshKeyB64);
// First command: Connects via SSHawait fetch(url + '/execute', { method: 'POST', body: JSON.stringify({ command: 'pwd' }) });
// Subsequent commands: Reuses connection (fast)await fetch(url + '/execute', { method: 'POST', body: JSON.stringify({ command: 'ls' }) });await fetch(url + '/execute', { method: 'POST', body: JSON.stringify({ command: 'df -h' }) });
// Connection stays open across requests—no reconnection overheadReset to change servers:
// Switch to different serverwindow.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 insights:
/keys/)This transforms server management: Your phone can now control production servers, run database backups, monitor logs, deploy code—all via HTTP, all without an SSH client.
The perfect pairing: Terminal + AI agent side-by-side in one browser window.
Hoody Terminal supports embedding hoody-agent directly in a side panel—giving you an AI 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.icu/ ?panel=https://PROJECT-CONTAINER-workspaces-1.hoody.icu &panel-width=40%What you get:
The workflow:
du -sh * | sort -hEach terminal can have its own dedicated agent:
# Terminal-1 with agent-1 (configured for frontend context)https://PROJECT-CONTAINER-terminal-1.hoody.icu/ ?panel=https://PROJECT-CONTAINER-workspaces-1.hoody.icu &panel-width=35%
# Terminal-2 with agent-2 (configured for backend context)https://PROJECT-CONTAINER-terminal-2.hoody.icu/ ?panel=https://PROJECT-CONTAINER-workspaces-2.hoody.icu &panel-width=35%
# Terminal-3 with agent-3 (configured for DevOps context)https://PROJECT-CONTAINER-terminal-3.hoody.icu/ ?panel=https://PROJECT-CONTAINER-workspaces-3.hoody.icu &panel-width=35%Each agent instance:
Bookmark these URLs—instant AI-assisted development environments.
1. Learning Linux/DevOps:
https://PROJECT-CONTAINER-terminal-1.hoody.icu/ ?panel=https://PROJECT-CONTAINER-workspaces-1.hoody.icu &panel-width=40%2. Debugging Issues:
https://PROJECT-CONTAINER-terminal-2.hoody.icu/ ?panel=https://PROJECT-CONTAINER-workspaces-2.hoody.icu &panel-width=35%tail -f /var/log/app.log3. Infrastructure Management:
https://PROJECT-CONTAINER-terminal-3.hoody.icu/ ?panel=https://PROJECT-CONTAINER-workspaces-3.hoody.icu &panel-width=40% &ssh_host=production-server.com &ssh_user=admin4. Code Review + Testing:
https://PROJECT-CONTAINER-terminal-4.hoody.icu/ ?panel=https://PROJECT-CONTAINER-workspaces-4.hoody.icu &panel-width=45%/app/srcnpm testWhat the agent can do from the panel:
Panel width customization:
&panel-width=30% # Smaller panel, more terminal space&panel-width=50% # Equal split&panel-width=60% # Larger panel for complex agent responsesPick based on your workflow—debugging needs more terminal space, learning needs more agent space.
Multiple agents for complex projects:
# Frontend terminal with agent-1 (configured for React/TypeScript)open "https://PROJECT-CONTAINER-terminal-1.hoody.icu/?panel=https://PROJECT-CONTAINER-workspaces-1.hoody.icu"
# Backend terminal with agent-2 (configured for Go/databases)open "https://PROJECT-CONTAINER-terminal-2.hoody.icu/?panel=https://PROJECT-CONTAINER-workspaces-2.hoody.icu"
# Database terminal with agent-3 (configured for PostgreSQL)open "https://PROJECT-CONTAINER-terminal-3.hoody.icu/?panel=https://PROJECT-CONTAINER-workspaces-3.hoody.icu&ssh_host=db.internal"Each agent instance configured differently:
All working in the same container—files created by one terminal visible to others, services accessible across terminals, perfect for full-stack development.
Traditional setup:
Hoody setup:
The URL is the environment. Share the URL, you share the entire setup—terminal, agent, SSH connection, panel layout, everything.
Perfect for:
See Web Terminal UI → for all panel customization options.
Query your container’s state via HTTP:
# Get CPU/memory/disk statshoody terminal system resources
# List running processes sorted by CPUhoody terminal processes list --sort cpu --limit 10
# Check what ports are listeninghoody terminal system ports
# View X11 displayshoody terminal system display-info// Get system resource statsconst resources = await containerClient.terminal.system.getResources();console.log(`CPU: ${resources.data.cpu}%, Memory: ${resources.data.memory}%`);
// List running processes sorted by CPUconst procs = await containerClient.terminal.system.listProcesses({ sort: 'cpu', limit: 10 });
// Check listening portsconst ports = await containerClient.terminal.system.listPorts();
// View X11 displaysconst displays = await containerClient.terminal.system.getDisplayInfo();# Get CPU/memory/disk statscurl "https://$PROJECT-$CONTAINER-terminal-1.$SERVER.containers.hoody.icu/api/v1/system/resources"
# List running processes sorted by CPUcurl "https://$PROJECT-$CONTAINER-terminal-1.$SERVER.containers.hoody.icu/api/v1/system/processes?sort=cpu&limit=10"
# Check what ports are listeningcurl "https://$PROJECT-$CONTAINER-terminal-1.$SERVER.containers.hoody.icu/api/v1/system/ports"
# View X11 displayscurl "https://$PROJECT-$CONTAINER-terminal-1.$SERVER.containers.hoody.icu/api/v1/system/displays"All system information accessible via HTTP endpoints. No need to SSH in and run commands—just query the API.
Execute commands on OTHER servers through hoody-terminal:
The container becomes an HTTP-to-SSH bridge. Your phone can now execute commands on your production servers via HTTP.
SSH Client (installed) → SSH Server (configured) → Shell (finally)Problems:
Any HTTP Client → hoody-terminal URL → Shell (immediately)Advantages:
<iframe src="terminal-url" />)Because terminals are HTTP:
Embed in Documentation
<iframe src="https://demo-terminal.hoody.icu/?cmd=bHMgLWxh&readonly=true" />Live terminals in your docs showing actual command execution.
Phone Executes Production Commands
// From mobile browserawait fetch(terminalUrl + '/execute', { method: 'POST', body: JSON.stringify({ command: 'pm2 restart api', wait: true })});AI Orchestrates Infrastructure
// AI agent deploys automaticallyconst steps = [ 'git pull origin main', 'npm install --production', 'npm run build', 'pm2 restart app'];
for (const cmd of steps) { await fetch(terminalUrl + '/execute?terminal_id=deploy', { method: 'POST', body: JSON.stringify({ command: cmd, wait: true }) });}Cascading Execution
// Terminal A executes command that triggers Terminal Bawait fetch(containerA_terminal + '/execute', { method: 'POST', body: JSON.stringify({ command: `curl -X POST ${containerB_terminal}/execute -d '{"command":"npm test"}'`, wait: true })});One-off commands with immediate results:
Start task asynchronously, check later:
// Use terminal-3 for build tasksconst buildTerminalUrl = 'https://PROJECT_ID-CONTAINER_ID-terminal-3.SERVER_NAME.containers.hoody.icu';
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 resultconst 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 in ${result.duration_ms}ms`); console.log(result.stdout); } else { setTimeout(checkStatus, 2000); }};
checkStatus();Check progress visually: Open the web terminal URL in your browser to watch the build running in real-time—same terminal-3 URL shows live output.
Mix workflows: Start command via API (from phone, script, CI/CD), then monitor progress visually in the web terminal from any browser.
Multi-step workflows—commands 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 deploymentsconst deployUrl = 'https://PROJECT_ID-CONTAINER_ID-terminal-2.SERVER_NAME.containers.hoody.icu';
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, disk } = await response.json();
const alerts = [];
if (cpu.usage_percent > 80) { alerts.push(`⚠️ CPU at ${cpu.usage_percent}%`); }
if (memory.usage_percent > 85) { alerts.push(`⚠️ Memory at ${memory.usage_percent}%`); }
if (disk.usage_percent > 90) { alerts.push(`⚠️ Disk at ${disk.usage_percent}%`); }
return alerts;}
// Run every 5 minutessetInterval(() => { const alerts = await checkHealth(terminalUrl); if (alerts.length > 0) { console.log('Health alerts:', alerts); }}, 300000);Collaborative debugging: Team member encounters a bug. Instead of screen sharing:
https://{project}-{container}-terminal-2.{server}.containers.hoody.icuNo screen share setup. No “can you see this?”. Just instant collaboration.
AI agents execute while you orchestrate:
// AI agent executes commands via the terminal HTTP APIconst TERMINAL_URL = 'https://PROJECT_ID-CONTAINER_ID-terminal-1.SERVER_NAME.containers.hoody.icu';
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.No SSH setup needed for automation:
# Deploy from GitHub Actions, GitLab CI, any CI/CDimport requests
terminal_url = os.environ['HOODY_TERMINAL_URL']
response = requests.post( f'{terminal_url}/api/v1/terminal/execute', json={ 'command': './deploy.sh production', 'terminal_id': 'ci', 'wait': False, 'timeout': 600 })
command_id = response.json()['command_id']# Monitor deployment status via command_idSolve issues together in real-time:
Support agent and customer both open:
https://{customer-container}-terminal-1.{server}.containers.hoody.icuBoth see the same terminal. Both can type. Agent fixes issue while customer watches. No more “type this command” followed by typing errors.
Admin your servers from anywhere:
Your phone’s browser → Terminal URL → Execute:
systemctl restart nginxtail -f /var/log/app.logdocker pshtopFull Linux shell on your phone. Because it’s just a URL.
Embed working terminals in docs:
<!-- Your documentation --><p>To check system status, run:</p>
<iframe src="https://demo-terminal.hoody.icu/?cmd=c3lzdGVtY3RsIHN0YXR1cyBuZ2lueA==&readonly=true" height="400"/>Readers see ACTUAL execution, not just code blocks. Interactive learning.
Group related commands using the same terminal URL to maintain session context:
// ✅ Good - Same terminal URL maintains stateconst deployUrl = 'https://PROJECT_ID-CONTAINER_ID-terminal-2.SERVER_NAME.containers.hoody.icu';
await fetch(deployUrl + '/execute', { method: 'POST', body: JSON.stringify({ command: 'cd /app', wait: true })});
await fetch(deployUrl + '/execute', { method: 'POST', body: JSON.stringify({ command: 'npm install', wait: true })});// Runs in /app (working directory preserved)
// ❌ Bad - Different terminal URLs = different sessionsawait fetch('...terminal-1.hoody.icu/execute', { body: JSON.stringify({ command: 'cd /app' })});
await fetch('...terminal-2.hoody.icu/execute', { body: JSON.stringify({ command: 'npm install' })});// Runs in ~ (terminal-2 has its own separate session)Remember: terminal-1, terminal-2, terminal-3 are all in the SAME container. They all see the same files, same services, same system. They just have independent shell sessions (separate working directories, separate command history).
Always use wait: false for commands that might take >3 seconds:
// ✅ Async for buildsawait fetch('.../execute', { body: JSON.stringify({ command: 'npm run build', wait: false, timeout: 300 })});
// ❌ Sync would block for minutesPrevent 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:
# List active sessionsGET /api/v1/terminal/sessions
# Delete old sessionsDELETE /api/v1/terminal/{terminal_id}For debugging or exploration, open the web URL directly:
https://{project}-{container}-terminal-1.{server}.containers.hoody.icuBetter than API for: typing multiple commands, seeing colored output, scrolling history.
Query resource endpoints before intensive operations:
const { cpu, memory, disk } = 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 user parameter when starting the terminal session. For security, consider using separate containers for privileged vs unprivileged operations.
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.
Absolutely. 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. No SDK needed.
Commands continue executing on the server. Terminal sessions are server-side, not browser-side. Close your laptop, command keeps running. Check status later via the /result/{command_id} endpoint.
Yes! Use iframes to embed terminal URLs directly in your app. Common pattern: documentation with live command execution, dashboards showing server logs, customer portals with diagnostic terminals.
Theoretically unlimited. Practically: hundreds of concurrent sessions work fine. Each session consumes minimal RAM (~10MB). Use different terminal_id values for isolated sessions or same terminal_id for shared/multiplayer sessions.
Yes! Native browser support on iOS and Android. The UI adapts to touch input, includes an on-screen keyboard option, and supports mobile gestures. Full Linux terminal from your phone’s browser.
Yes, via GET /api/v1/terminal/screenshot?terminal_id=1&format=png. Returns visual snapshot as PNG/JPEG/GIF. Useful for documentation, tutorials, or monitoring dashboards showing live terminal state.
Use GET /api/v1/terminal/history/{terminal_id} to retrieve all commands executed, their exit codes, and execution times. Perfect for auditing, debugging, or understanding what changed in a session.
Check container is running:
curl "https://api.hoody.icu/api/v1/containers/{container_id}?runtime=true" \ -H "Authorization: Bearer $HOODY_TOKEN"Verify status: "running" and runtime_info.terminals shows active sessions.
Start container if stopped:
curl -X POST "https://api.hoody.icu/api/v1/containers/{container_id}/start" \ -H "Authorization: Bearer $HOODY_TOKEN"Possible causes:
Quick test:
# Direct browser access (not iframe)https://{project}-{container}-terminal-1.{server}.containers.hoody.icuFor 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 timeout if needed:
{ timeout: 600 } // 10 minutes for large buildsCheck stderr, not just exit_code:
const result = await execute({ command: 'npm test', wait: true });
// Some commands exit 0 but write errors to stderrif (result.stderr.includes('ERROR') || result.stderr.includes('FAIL')) { console.error('Command had errors:', result.stderr);}Ensure using same terminal_id:
// ✅ Correct - Same sessionterminal_id: "prod-session" // State persists
// ❌ Wrong - Different sessionsterminal_id: Math.random() // New session each timeSessions are deleted on:
Check SSH parameters:
{ command: 'ls', ssh_host: 'prod.example.com', ssh_port: 22, // Default if omitted ssh_user: 'admin', ssh_password: 'your-password' // Or ssh_key}Verify SSH connectivity:
# Test from container firstssh admin@prod.example.comCommon issues:
StrictHostKeyChecking=no, so unknown hosts are accepted automatically)Explore other interactive services:
Displays
Full desktop environments accessible via URL—run VS Code, browsers, any GUI application.
Browser
Chrome automation as REST API—control browsers via HTTP, scrape websites, run tests.
Exec
Transform any script into an HTTP endpoint—your code becomes an API automatically.
Master terminal workflows:
Your shell is a URL.
Execute from anywhere.
Collaborate in real-time.
AI-native by design.
This is how terminals work in the HTTP era.