Hoody Code spawns isolated VS Code instances on demand through HTTP requests. You can embed a single extension in your own application, password-protect an environment, and manage several development workspaces from one orchestrator.
The current build is based on VS Code 1.137. Containers run it on Node.js 26, the version mounted at /hoody/plugins/node; Node.js 24 is the minimum.
Capabilities
Section titled “Capabilities”- Spawn instances - Create a VS Code environment with one HTTP request
- Extension embedding - Open a single already-installed extension in an isolated view
- Password protection - Require a password before an instance is reachable
- Multi-workspace - Independent settings and extensions per instance
- Instance isolation - Separate data directories per instance
- Health monitoring - Track orchestrator status and running instances
- Custom configuration - Pass CLI flags via query parameters
- Instance reuse - Existing instances are reused instead of re-spawned
Two servers, two hostnames
Section titled “Two servers, two hostnames”Hoody Code is two processes, and knowing which one answers a request saves a lot of confusion.
The orchestrator answers on the container’s code URL. It spawns and reuses instances, and that is nearly all it does:
https://PROJECT-CONTAINER-code-1.SERVER.containers.hoody.comEach spawned VS Code instance is its own server on its own port, reached through the container’s HTTP proxy hostname for that port:
https://PROJECT-CONTAINER-http-PORT.SERVER.containers.hoody.comSpawning returns a page whose iframe points at that second hostname. Everything inside the editor belongs to the instance rather than the orchestrator, and so does login: the orchestrator has no login or logout handler of its own.
API Endpoints Summary
Section titled “API Endpoints Summary”On the orchestrator (https://PROJECT-CONTAINER-code-1.SERVER.containers.hoody.com):
GET /?folder=/pathandGET /api/v1/code?folder=/path- Spawn or reuse an instance and return the wrapper page whose iframe points at it. Both paths are the same handler, so either one worksGET /api/v1/code/health- Orchestrator health checkGET /api/v1/code/version- The version of the running orchestrator and of the packaged editor it spawns, reported as separate figures. The two are built and deployed independently, so a single number would describe one and imply the other. It never contacts the networkGET /api/v1/code/manifest.json- Web application manifest, for installing Code as an application from this originGET /status- Orchestrator settings (port, base port, data directory) and every running instance with its id, port, folder, start time and uptimePOST /api/v1/code/extensions/install- Stage a.vsixfrom a URL so instances install it when they next start. See ExtensionsGET /api/v1/code/extensions/list- What is staged, and what the instance appears to have installedGET /openapi.yaml- The service’s API description. The root path serves the same document, asapplication/x-yaml, when a request carries neitherfoldernorid. That is the discovery path: on acode-Nhostname the proxy suppliesid, so a plainGET /there spawns or reports a parameter error insteadGET /favicon.ico,GET /robots.txt, andGET /security.txt(also at/.well-known/security.txt)
That list is the orchestrator’s entire documented surface.
The editor version is pinned in the service rather than tracked against a release feed. GET /api/v1/code/version reports the versions actually running. Code does not check for releases of itself: there is no update-check route on the orchestrator or on an instance, and no request to any release feed. The editor still checks Open VSX for updates to extensions installed from there, which is a different thing and is described under Extensions. Everything else a browser loads while the editor is open belongs to the instance, on the instance’s own hostname.
On a spawned instance (https://PROJECT-CONTAINER-http-PORT.SERVER.containers.hoody.com):
GET /api/v1/code- The VS Code workbench itself, also mounted at/vscodeand at the instance’s rootGET /login,POST /login(password mode only) andGET /logout(whenever authentication is enabled) - Login form, credential submit, and session logoutPOST /api/v1/code/mint-key- Generate or retrieve the server’s web key halfGET /proxy/{port}/{path}andGET /absproxy/{port}/{path}- Forward to another port inside the container. These sit at the instance’s root, not under a/api/v1/codeprefix; see Forwarded ports and the session cookie
Two spawn responses report a problem rather than a page. 409 means the port that instance would use is already held by a process the service did not start, so retrying the same id will not help until that process goes. 503 means an instance was started but never confirmed it was serving, and it is cleaned up rather than left half-running.
There is no stop or delete endpoint. An instance runs until its process ends, so cleanup happens through the container: stop the process, or restart the Code service. restart=true on a spawn request respawns one instance without touching the others.
Basic instance
Section titled “Basic instance”Spawn VS Code for a folder:
# Spawn or reuse instance 1 for a folder. Returns the wrapper HTML page,# whose iframe points at the instance's own hostname. The spawn is the# orchestrator's root path; the proxy fills in the matching id.curl "https://PROJECT-CONTAINER-code-1.SERVER.containers.hoody.com/?folder=/home/user/my-project"
# Check orchestrator healthcurl "https://PROJECT-CONTAINER-code-1.SERVER.containers.hoody.com/api/v1/code/health"# Check orchestrator healthhoody code health -c <container-id>import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });
// Check orchestrator healthconst health = await containerClient.code.kit.getHealth();One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Spawns or reuses instance 1 for the given folder, and checks the orchestrator’s health. Paste either link straight into a browser.
# Spawn
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT-CONTAINER-code-1.SERVER.containers.hoody.com/?folder=/home/user/my-project&method=GET&response=transparent
# Health
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT-CONTAINER-code-1.SERVER.containers.hoody.com/api/v1/code/health&method=GET&response=transparent The spawn takes two parameters, folder and a numeric id, and the orchestrator requires both. On a code-N hostname the proxy forces id to N, so it cannot be repointed at another instance from the query string, and it can fill in a configured default folder when the request omits one. Supply one without the other and the orchestrator answers with a parameter error. The whole query string is capped at 8192 bytes, and anything longer is refused with a parameter error whichever parameters it carries. Because the proxy always supplies id, an unqualified GET / on a code-N hostname is that error rather than a listing, unless the deployment configured a default folder. id must be a plain non-negative integer. Trailing characters, a decimal point, or a sign are all rejected, and so is a repeated id. Only the exact parameter name counts: a bracketed spelling such as id[] is ignored rather than merged into it, so a request cannot be steered to an instance other than the one its hostname grants. The largest usable id depends on the service’s base port, because an instance binds that port plus its id.
Extension-only mode
Section titled “Extension-only mode”Add extension to a spawn request and VS Code hides the file explorer, leaving only that extension’s UI. The value is the PUBLISHER.NAME id, so ms-azuretools.vscode-docker works the same way:
# Spawn instance 1 in extension-only modecurl "https://PROJECT-CONTAINER-code-1.SERVER.containers.hoody.com/?folder=/workspace&extension=ms-python.python"Password protection
Section titled “Password protection”Set password authentication with a CLI flag when the Code service is launched. Login is served by the instance, on its own port, at bare /login and /logout. The orchestrator at code-1 has no login handler at all, so a request to /api/v1/code/login there never reaches a form:
# Submit login credentials to the instancecurl -X POST "https://PROJECT-CONTAINER-http-PORT.SERVER.containers.hoody.com/login" \ -d 'password=my-password'GET /login returns the form and GET /logout clears the session; POST login exists in password mode, logout whenever authentication is enabled.
Type the flag value, not a hash of it. The orchestrator’s --password help calls its argument a SHA-256 digest, but nothing computes or verifies a digest: the string you pass reaches the instance as $PASSWORD and is compared with the submitted form value literally. Whatever you gave the flag is what the login form expects, digest-looking or not.
Failed logins are rate-limited. The instance keeps two token buckets shared across all its login requests: 2 per minute and 12 per hour. A login is allowed while either bucket still has a token, so a burst of wrong passwords gets roughly fourteen attempts before the form starts refusing. Successful logins return before a token is spent and do not count against either bucket; the availability check runs before the password is checked, though, so exhausted buckets briefly block a correct password too. Wait for the minute bucket to refill.
The mint-key endpoint is separate. It generates or retrieves the server’s 32-byte web key half used for secure communications, and is not a password API. It is served by the instance and requires the same authentication as the workbench:
# Generate/retrieve server web key (binary response)curl -X POST "https://PROJECT-CONTAINER-http-PORT.SERVER.containers.hoody.com/api/v1/code/mint-key" \ -H "Cookie: hoody-code-auth=SESSION_TOKEN"Without that session cookie the instance answers 401 or redirects to the login form.
Forwarded ports and the session cookie
Section titled “Forwarded ports and the session cookie”A password-protected instance stores the login token in a cookie named hoody-code-auth (or hoody-code-auth-<suffix> when the server runs with --cookie-suffix). The cookie is HttpOnly, SameSite=Lax, Secure when the instance is reached over HTTPS, and has no Domain attribute, so it belongs to the instance’s own address only. To reach other ports through the same login, use the instance’s /proxy/<port>/ and /absproxy/<port>/ paths. Requests that the instance forwards to another local port, through /proxy/<port>/, /absproxy/<port>/, or a hostname matching its --proxy-domain template, have the login cookie removed before they leave the instance, under its current name and under the name earlier versions used (hoody-code-session, or hoody-code-session-<suffix>). This applies to plain HTTP requests and to WebSocket upgrades. An application listening on a forwarded port therefore never sees the token; every other cookie in the request reaches it unchanged.
Instance isolation
Section titled “Instance isolation”Each instance keeps its own state:
- Installed extensions live in that instance’s own directory, under
<data-dir>/<id>/extensions - User settings and keybindings are stored separately
- The last opened folder or workspace is saved in settings for the next session
Instance lifecycle
Section titled “Instance lifecycle”First request - Fresh spawn:
- Shows a loading overlay
- Spawns the VS Code process
- Configures it from the request parameters
- Returns the iframe once it is ready
Subsequent requests - Reuse:
- No loading overlay, unless the instance is still starting because another request asked for it first, in which case this request waits for that start and shows the overlay too
- Reuses the existing instance rather than re-spawning it
- Keeps the same state
- Opens the folder the instance was started with. A later request naming the same
idand a differentfolderis answered from the running instance and itsfolderis ignored, with no error. Setrestartto open a different folder
Custom configuration
Section titled “Custom configuration”Use the supported query parameters to configure an instance:
# Spawn instance 1 with the French UI localecurl "https://PROJECT-CONTAINER-code-1.SERVER.containers.hoody.com/?folder=/workspace&locale=fr"Supported parameters:
folder- Required absolute folder path.id- Required numeric instance id, supplied by thecode-Nhostname.extension- Optional extension-only mode, given asPUBLISHER.NAME.restart- Optional. Set it truthy and a running instance is respawned instead of reused. This is the only way to restart an instance through the API.locale- Optional IETF language tag such asfr.app-name- Optional product name shown in the workbench.proxy-domain- Optional proxy domain for the instance.welcome-iframe-url- Optional URL that replaces the Welcome page with a full-screen iframe.page-loader- Optional boolean. Enables the loading overlay.page-loader-path- Optional path to a custom loader HTML file.disable-walkthroughs- Optional boolean. Suppresses VS Code walkthroughs.hoody-code- Optional boolean. Enables the Hoody Code branding layer.
folder, id, extension and restart are handled by the orchestrator itself; the remaining eight are forwarded to the instance as flags.
workspace and ew are not on the orchestrator’s allowlist and are dropped from a spawn URL, though the SDK and the instance’s own routes still accept them.
The orchestrator passes flags through an allowlist. Anything outside it is dropped without an error, so a parameter that looks accepted may simply have been ignored.
Extensions
Section titled “Extensions”The editor has a marketplace. The Extensions view inside a spawned instance searches and installs from Open VSX, and the editor checks there for updates to what it installed. An extension installed that way belongs to that one instance and lives in that instance’s own data directory.
The two routes below put an extension in front of every instance instead of one: a directory in the container, and an HTTP call. Use them for an extension that is not on Open VSX, for a specific build, or to have instances come up with it already installed.
Every spawned instance is started with --preload-builtin-extensions-dir, defaulting to /hoody/storage/hoody-code/extensions. Drop VSIX files in that directory and each new instance comes up with them already installed, with no per-instance install step. Point the orchestrator elsewhere with its own --preload-builtin-extensions-dir flag at launch.
POST /api/v1/code/extensions/install writes into that same directory from a URL, so you can add an extension without shell access to the container:
curl -X POST "https://PROJECT-CONTAINER-code-1.SERVER.containers.hoody.com/api/v1/code/extensions/install?id=1" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/publisher.name-1.2.3.vsix"}'The JSON request body is at most 64 KiB (65,536 bytes); a larger one is refused with 413 request-too-large.
Staging is not installing. The editor installs what it finds in that directory only while an instance is starting, so a staged extension reaches a running instance at its next start and not before. The response says so in appliesAt and requiresRestartToApply, and it never reports an extension as installed. Restart an instance by spawning it again with restart set.
The directory is shared by every instance on one orchestrator, so an extension staged through one instance is installed by all of them. The response reports that as stageScope.
One version of an extension is kept at a time, identified by the publisher and name inside the archive rather than by its filename. Staging a newer version replaces the older one, and the response names what it replaced. Staging an older version is refused unless the request body sets "allowDowngrade": true. Staging bytes that are already there changes nothing and reports unchanged.
The target platform is not part of that identity. The editor installs every staged file it finds, and two files claiming one extension leave it with a choice it cannot make, so a platform-specific build replaces a universal one and the reverse. The response lists anything it removed that way under removedDuplicates.
The archive is fetched over http or https only, with no credentials in the URL and no address inside the container or on a private network, checked again on every redirect. A destination that is not permitted is refused with 403. Repeating the request will not change that on its own, and nothing you can send widens the policy, but it is not a verdict about the future either: what a hostname resolves to, and where a redirect points, are chosen elsewhere and can change.
A download that fails on the far side answers 502 and may be worth retrying. So does a redirect that leads somewhere the service will not follow, including one to a scheme other than http or https: the URL you sent was usable, and the far side chose the hop that was not.
GET /api/v1/code/extensions/list reports two things and keeps them apart. desired is what is staged; observed is what that instance appears to have installed. The two disagree between a stage and the next restart, which is the normal case and is why both are reported. A section the service could not read reports unavailable with a reason instead of an empty list, and partial is true when either section is in that state.
Stage before starting an instance rather than during. An instance installs staged extensions as it starts, and a stage that lands inside that window can leave the instance recording one version’s details while holding another version’s contents. Both versions are ones the service accepted, so nothing is corrupt; the pairing is wrong. GET /api/v1/code/extensions/list shows the mismatch, and restarting the instance installs whatever is staged at that point.
Removal is not part of this API. Taking a file out of the stage directory stops new instances from installing it; it does not uninstall it from an instance that already has it.
Options the wrapper does not model
Section titled “Options the wrapper does not model”VS Code’s server accepts more options than Hoody Code exposes as flags. --vscode-option passes one of them straight through. It is a flag on the workbench server, not a query parameter and not a flag of the service that runs in a container: the orchestrator’s allowlist drops vscode-option from a spawn URL, and the orchestrator’s own command line does not model it either.
In a container, set VSCODE_OPTIONS in the Code service environment. Instances inherit the service environment, and each one appends those entries to its own options:
# In the Code service environment. Space-separated; same syntax as the flag.VSCODE_OPTIONS="--link-protection-trusted-domains=https://docs.example enable-smoke-test-driver"The flag itself applies when you run the workbench server directly, outside the orchestrator:
# A bare flag means true; repeating a flag builds a list.hoody-code --vscode-option enable-smoke-test-driver \ --vscode-option link-protection-trusted-domains=https://docs.example \ --vscode-option link-protection-trusted-domains=https://wiki.exampleThe rules:
flag=valuesets a string; a bareflagsetstrue; the valuestrueandfalsebecome booleans.- Repeating a flag collects its values into a list. Options that Hoody Code itself models as lists, such as
link-protection-trusted-domains, are always sent as a list, even when given once. - A leading
--on the flag is optional, but use the--vscode-option=--flag=valuespelling for it, because a separate--flagtoken is parsed as another option. VSCODE_OPTIONSin the environment holds space-separated entries, appended to any given on the command line. Entries follow the same syntax, and the same validation rejects malformed ones.- Flag names consist of letters, digits and dashes, as VS Code’s own do (
enable-smoke-test-driver,logsPath). An entry without a flag name (=value), with other characters in the name, using a JavaScript object-member name such asconstructor, or namingvscode-optionitself stops the server at startup with an error.
Health monitoring
Section titled “Health monitoring”The health check is shown in Basic instance above. Its response:
{ "status": "ok", "service": "hoody-code", "started": "2024-01-15T10:30:00.000Z", "built": "2024-01-10T09:00:00.000Z", "pid": 1234, "ip": "10.0.0.12", "memory": { "rss": 44145050, "heap": 29884416 }, "fds": 42, "userAgent": "HoodyMonitor/1.0"}Use Cases
Section titled “Use Cases”Multi-tenant development
Section titled “Multi-tenant development”Give each user their own instance. Settings, extensions and session state stay separate. Instances share the container’s filesystem and run under the same service account, so they are not a boundary between people who do not trust each other: give those users separate containers.
Embedded development tools
Section titled “Embedded development tools”Embed a single extension in your own application with extension-only mode, which hides the file explorer and leaves just that extension’s UI.
Educational platforms
Section titled “Educational platforms”Run per-student environments: password-protect instances, pre-configure extensions, monitor health, and clean up between sessions. A password controls who reaches an instance; it does not confine what commands or extensions can do inside the container, so use one container per student where that matters.
CI/CD automation
Section titled “CI/CD automation”Automate code edits by spawning a temporary instance, driving it through extensions, cleaning up when the job finishes, and integrating the whole run with your build pipeline.
Extension showcases
Section titled “Extension showcases”Demonstrate a VS Code extension by embedding an instance in documentation. Readers get an interactive demo of what the extension does and install nothing.
Best Practices
Section titled “Best Practices”Instance strategy
Section titled “Instance strategy”Use a separate folder per project to keep files and editor state apart, and plan capacity for the load you expect. A folder is not a boundary: an instance can read what its account can read, so give mutually untrusted users separate containers.
Folder paths
Section titled “Folder paths”Always use absolute paths such as /home/user/project. Do not use relative paths or .., validate paths to prevent traversal, and make sure the folder exists before spawning.
Password security
Section titled “Password security”Set the password with a CLI flag when launching the Code service, rotate it regularly, and remember that URLs can leak through logs and browser history.
Resource management
Section titled “Resource management”Monitor health via /api/v1/code/health, clean up old instances, and track disk usage in the data directories.
Extension configuration
Section titled “Extension configuration”Validate extension IDs against the PUBLISHER.NAME format, test compatibility before embedding, document system dependencies, and pin versions for consistency.
Useful Questions
Section titled “Useful Questions”Q: How many instances can I run? The practical limit is the container’s memory and CPU.
Q: Do instances persist after restart? Data directories persist, including extensions and settings. Running processes do not, so you re-spawn them.
Q: Can I customize VS Code appearance?
Use the locale query parameter for the display language, load custom CSS/JS via the --external-js/--external-css server flags, and configure further through extension settings.
Q: How do I update extensions?
For an extension installed from Open VSX, update it from the Extensions view inside the instance. For a staged one, stage the newer version with POST /api/v1/code/extensions/install and restart the instance. A staged version replaces the older one for every instance on the orchestrator; updating from the editor changes one instance only.
Q: What happens on first request vs. a reused instance?
A fresh spawn starts a new VS Code process and (when --page-loader is enabled) shows a branded loading overlay during initialization; a reused instance loads its existing process without re-spawning.
Q: Can I run multiple Code services?
Yes. Use different instance numbers such as code-1 and code-2. Each one has its own orchestrator, port range, and data directory.
Q: How do I embed in my app?
Request the URL with the extension parameter, embed the returned page in an iframe, handle authentication if the instance needs it, and monitor the health endpoint.
Troubleshooting
Section titled “Troubleshooting”Instance won’t start
Section titled “Instance won’t start”Cause: Folder not found, invalid parameters.
Solution: Verify the folder exists and is an absolute path, confirm the folder parameter (if used) is passed correctly, and check the orchestrator logs.
Extension not loading
Section titled “Extension not loading”Cause: Invalid extension ID or web-incompatible extension.
Solution: Verify the ID format PUBLISHER.NAME, check that the extension supports the web version, test without extension-only mode first, and confirm the ID on the marketplace.
Password authentication fails
Section titled “Password authentication fails”Cause: Incorrect password or login endpoint misuse. Solution: Verify the password matches the one configured via CLI flag, check the URL encoding of the form body, and confirm the service was started with password authentication enabled.
High resource usage
Section titled “High resource usage”Cause: Too many instances running.
Solution: Check health with /api/v1/code/health, adopt an instance cleanup policy, and increase server resources.
Instance state lost
Section titled “Instance state lost”Cause: Data directory cleared. Solution: Do not delete the data directories of active instances, and back up critical instance data.
Health check fails
Section titled “Health check fails”Cause: Orchestrator down or network issue. Solution: Verify the orchestrator process is running, check network connectivity to the Code Orchestrator URL, and review orchestrator logs for errors.