Imported from williamsharkey/shiro (
AGENTS.md). Install upstream withnpx skills add williamsharkey/shiro. Copyright stays with the author.
AGENTS.md
Canonical agent instructions live here. CLAUDE.md is a compatibility shim and should only point back to this file.
Mission
Shiro is a browser-native Unix-like development environment. Prioritize changes that make it feel more like a real machine in the browser: shell, filesystem, Node/npm, editors, build tools, networking, WASI/x86, and first-class AI tooling.
Do not treat the dashboard or wrappers as the product. The product is the browser OS itself.
Architecture Snapshot
src/main.ts: boot, filesystem init, command registration, seeded runtime hydration.src/filesystem.ts: IndexedDB-backed POSIX-like filesystem.src/shell.ts: bash-like parser/executor, pipes, redirects, jobs, functions, arrays, traps.src/terminal.ts: xterm integration and input handling.src/commands/*: one command per file or small group.src/node-compat/*: Node.js runtime shims used bynodeand Claude Code.src/wasi-runtime.ts+src/wasi-packages.ts: Tier 2 WASI support.src/x86/*: Tier 3 x86-64 emulator.src/commands/seed.ts,src/commands/hc.ts,src/seed-runtime-context.ts: seeded sessions, host-page access, runtime orientation.src/claude-config.ts,src/node-compat/preload.ts,src/node-compat/process.ts: Claude bootstrap, auth persistence, startup defaults.server.mjs: static hosting, API proxying, OAuth callback, signaling, relay.
Working Style
- Prefer small, direct fixes over speculative rewrites.
- Read the surrounding code before editing. Shiro has a lot of compatibility shims and edge cases.
- Use
rg/rg --filesfor search. - Use
apply_patchfor file edits. - Do not revert unrelated work in a dirty tree.
- Do not hard-code counts, build numbers, or implementation inventories unless you just verified them.
Commands
When adding a command:
- Add a file under
src/commands/. - Export a
Command. - Register it in
src/main.ts. - Lazy-load it if it is large or rarely used.
Core pattern:
export const myCmd: Command = {
name: 'mycmd',
description: 'Does something useful',
async exec(ctx) {
ctx.stdout = '...\n';
return 0;
},
};
Seeded Sessions And Inner Claude
- Seeded boots write runtime context to
/home/user/NEO.mdand/home/user/.shiro-context.json. - If a seeded session has host-page access, inner Claude should learn that from
NEO.mdand usually start withhc outer. - Keep seeded agent guidance compact and current in
src/claude-md-seed.ts. CLAUDE.mdis still written for compatibility, but it should only redirect toAGENTS.mdplusNEO.md.
Claude Code In Shiro
claudeis a Shiro builtin (src/commands/claude.ts) wrapping the npm CLI: it installs the pinned build if needed, opens the sign-in panel (src/claude-signin.ts) when there are no credentials, and adds--dangerously-skip-permissionsfor sessions.claude-window(old namesc) runs it in a new window.- The npm package is pinned to 2.1.112, the last pure-JS release (
src/claude-code-version.ts). Boot installs it in the background from the npm tarball. At load timeexecution.tsrewrites its inlinedVERSIONconstant toCLAUDE_CODE_REPORTED_VERSION, because the API gates newer models (e.g.claude-opus-5-5, the defaultANTHROPIC_MODEL) on the version in the billing header. - The same load-time rewrite teaches 2.1.112 about newer models (
CAPABILITY_PATCHES): Opus/Sonnet 5+ get adaptive thinking, effort control including xhigh, and the xhigh launch default, like Opus 4.7. Without it the pinned build sent fixed-budget thinking, no effort, and defaulted subscriptions to medium effort. Opus 5.5 delivers thinking as one chunk at the end, so the token counter stalls during long thinking; that is the API, not a hang. - Also patched: interactive sessions use the in-memory
TodoWritelist instead of the file-backed task tools (TaskCreate…), whose proper-lockfile locking hung in Shiro;CLAUDE_CODE_ENABLE_TASKS=1restores them. Callback and promisefs.rmdirnow remove directories (they usedunlink, so lock directories never released). - The bundle patch also replaces Claude Code's Opus 4.7 launch card with a Shiro one and names
claude-opus-5-5"Opus 5.5". - Tiling panes (
src/panes.ts): every pane in#shiro-paneshas tiny corner triangles; dragging one splits the pane (mostly sideways = side by side, mostly vertical = stacked), dividers drag to resize (double-click evens them), andexit/Ctrl-D at an extra pane's prompt closes it.#terminalstays the main pane (window.__shiro.terminal); extra panes get fresh shells (copied env and cwd, no~/.profile). The layout is saved in localStorageshiro-panesand rebuilt on reload;window.__shiroPanes.layout()/.reset()inspect or clear it. Don't hand-build split layouts with page scripts anymore. - The startup banner (
drawHudinterminal.ts) listsclaude,gh auth login,remote start, and help/files/github;shiro://cmd/<cmd>links type the command at an idle prompt. A terminal that is busy (command running or alternate screen) never gets in-place banner rewrites; remote status goes to an idle sibling terminal instead (e.g. the upper shell of a split), drawing a banner there if needed.helpis a curated getting-started page;help --alllists every command. - Claude defaults in
process.ts: tool concurrency 4 and 3 planner agents (were 1), background tasks off. Export the env vars to override. - Scripts run through bin symlinks execute under their real path (
shell.ts→fs.realpath), so Claude-specific preload/env tweaks key off/@anthropic-ai/claude-code/. - Claude's
tuisetting is seeded tofullscreen(alt-screen renderer). The classic renderer leaves stale frames in scrollback when the window is resized or a frame is taller than the terminal. - Commands run through the
child_processshim execute in a forked shell with no terminal, so their output returns to the caller instead of painting over Claude's UI. - Console output is captured from boot into a bounded log (
src/console-log.ts): the newest 3000 entries / 1.5M characters, 2000 characters per entry, consecutive repeats collapsed, text only (no retained objects). The newest 300 entries are saved tolocalStorage(shiro-console-log) every 5s and on pagehide, so the previous page load's log is queryable after a reload or crash. Query it withconsole -g RE [--since S] [--prev], the remote{type:'console'}request, the shiro-mcpconsoletool, orcurl 'localhost:7788/console?grep=RE&level=error&since=-600000&limit=100&previous=1'via the probe. Replies default to 100 entries / 64 KB. - To profile a live session:
remote startin Shiro, thennode shiro-mcp/probe.mjs <code>(runnpm installinshiro-mcp/first; setSHIRO_SIGNALING_URLfor subdomains likehttps://music.shiro.computer). Every 2s it logs heap, long tasks, page response time, IndexedDB filesystem traffic, errors, tagged console lines, and shell commands still running after 30s intoprobe.jsonl, and servescurl localhost:7788/eval --data-binary '<js>'and/exec. Replies over the data channel are size-limited; fetch large files in ~100 KB slices. - Fullscreen TUIs copy a selection with OSC 52; both terminals handle it (
src/utils/osc52.ts, with anexecCommandfallback), andpbcopy/xclip/wl-copyare builtins that copy stdin. - Claude's Bash tool opens its task output file with
fs.promises.openand reads it back withhandle.read()insideawait using, so the shim's FileHandle must keep a real registered fd,read,stat, and[Symbol.asyncDispose]. - Claude auth/bootstrap lives in
src/claude-signin.ts,src/claude-auth.ts,src/claude-config.ts,src/node-compat/preload.ts, andsrc/node-compat/process.ts. - Shiro pre-seeds trust/onboarding/bypass settings for Claude Code.
- Browser-hosted Claude is more stable with conservative runtime defaults. Prefer serial/single-lane behavior over background worker fan-out unless you have verified a broader mode works.
- In
seed blob, Claude runs cross-origin from the host page. Shiro-backed calls must resolve through the Shiro origin, not the parent site, andserver.mjsCORS preflight handling must tolerate Claude headers likex-appandx-stainless-*.
Git And GitHub
gitis isomorphic-git through the/git-proxy/route inserver.mjs. The proxy dropsWWW-Authenticatefrom responses: a same-origin 401 carrying it makes the browser show a native login prompt that stalls the request, and every later one to the origin, until the command times out.githubAuth()insrc/commands/git.tssends the token on the first request (x-access-tokenbasic auth) for github.com remotes only, and cancels on auth failure instead of retrying. Clone, push, fetch, and pull use it. The token comes fromGITHUB_TOKEN/GH_TOKENorlocalStorage.shiro_github_token(gh auth login --with-token).gh auth login(no flags) is GitHub's OAuth device flow (src/github-auth.ts): it prints the one-time code like gh, opens a panel with Copy/Open buttons (even without a terminal, so it works from Claude's!mode and Bash tool), polls through the server's/api/github-login/route (only POST to/login/device/codeand/login/oauth/access_tokenis allowed), saves the token tolocalStorage.shiro_github_token, and fills in~/.gitconfiguser.name/email from the account if unset.gh auth refresh -s <scopes>re-authorizes with extra scopes. The OAuth app is "shiro.computer" (GITHUB_OAUTH_CLIENT_ID), owned by williamsharkey.- Spawns like Claude's
zsh -c -l <cmd>: flags after-care skipped (extractShellArgs), otherwise a cwd prefix turned-linto a command. git configsupports get/set/--list/--unset, local and--global(~/.gitconfig). Commit authors come from repo config, then~/.gitconfig, thenGIT_AUTHOR_*.gh(src/commands/gh*.ts) follows the real CLI's flags where implemented:auth(status/login/logout/token/setup-git),repo(view--json, list, create--source --push, clone, delete--yes),api(-q/--jq,-f/-F,-X/--method),pr,issue,release,workflow,run,label,search.- Known gaps: git only works from the repository root (no upward
.gitdiscovery), andgit log --onelineprints full messages.
Node Processes Share The Page
- Every
nodescript runs in the same page as the shell and every other script. Claude Code runs for hours while its tool calls start and finish other scripts, so a script must never remove globals on exit that another might still use.setImmediateis polyfilled once and never removed; deleting it from a finishing child hung Claude's Bash tool. - When a script exits it restores
fetch/setTimeout/clearTimeoutonly if the global is still the one it installed (restoreGlobalsinexecution.ts). Restoring blindly let~/.profile-launched autostart scripts clobber Claude's Node-stylesetTimeout, and Claude crashed with.unref is not a functionon the first message. - The terminal skips its startup prompt when
~/.profilelaunched a command throughinjectInput; that command prints the prompt when it finishes. - The module transform still assigns global
setTimeout/setIntervalwrappers for some bundles without restoring them. It's harmless so far, but it has the same problem.
Build, Test, Deploy
npx tsc --noEmit
cd tests && npm run test:shiro
npm run build
npm run deploy
Use focused vitest runs while iterating, then run the smallest meaningful verification set before deploy. For changes touching seed/Claude/bootstrap paths, relevant files usually include:
tests/tests/shiro-vitest/seed.test.tstests/tests/shiro-vitest/seed-runtime-context.test.tstests/tests/shiro-vitest/claude-bootstrap.test.tstests/tests/shiro-vitest/node-runtime.test.tstests/tests/shiro-vitest/new-features.test.tstests/tests/shiro-vitest/server-cors.test.ts
Production is https://shiro.computer on a DigitalOcean droplet. deploy.sh handles build, upload, and restart, and it is the only place that should bump build-number.txt. nginx on the host sets client_max_body_size 100m (/etc/nginx/sites-enabled/shiro): the 1 MB default rejected long Claude conversations and GitHub blob uploads with 413. deploy.sh uploads only server.mjs; the host's own /opt/shiro/package.json holds its deps (ws, and undici so proxied model calls have no 5-minute header timeout). Each model call logs one [proxy] messages model=… stream=… bytes=… → status headers in Nms line (journalctl -u shiro).
Gotchas
child_processis shimmed. There is no real process tree.- Most filesystem work is async under the hood even when sync APIs are emulated.
- Background-task-heavy or highly concurrent agent flows can stall in the browser runtime.
seedandseed blobare not equivalent. Preserve their runtime-context differences.- Expansion results are data:
$VAR,${NAME},$1–$9, and$(...)/backtick output have their quotes,\,$, and backticks swapped for private-use stand-ins (protectExpansioninshell.ts) before the command text is tokenized, and swapped back whenparseSegmentfinalizes args, redirect targets, and here-strings (restoreExpansion). Without this, JSON in variables lost its quotes and$(echo '$HOME')expanded."$@"is left alone (it relies on embedded quotes). - A loop,
if,case, or subshell can head a pipeline (for …; done | tail -1):splitTopLevelPipesfinds the pipe after the closing keyword andrunHeadedPipelinefeeds the head's output to the rest. Redirections afterdone/fi/esac(< in,> out,2>&1) are applied bysplitCompoundRedirects.printf(except-v) is the regular command, so redirects and pipes apply;ctx.stdoutIsTTYis false for piped/redirected commands (lsthen prints one name per line). - Node scripts end like node on an empty event loop: after the synchronous part, the runner waits until nothing tracked is in flight (
fetch,fs.promises, timers; seenode-compat/activity.ts) and output has been quiet for 60–150 ms, with the old 10 s ceiling as a fallback. Missing Node APIs come fromauto-stub.ts, which logs[AutoStub] called missing …the first time; a stubbed callback API never calls back, so check the console for these when something hangs. require('sharp')is a browser-backed implementation (shims/browser-sharp.ts: createImageBitmap + canvas, real metadata/resize/JPEG/PNG/WebP). Claude Code's image loader is patched to use it; its bundled native/sharp path stalled image Reads.- Per-command env (
NAME=value cmd) is applied for that pipeline only (splitEnvPrefixinshell.ts), and>&2/1>&2duplicate onto stderr left to right, as in bash. lsprints columns even when piped.- Keep docs unified: update
AGENTS.mdfirst, keepCLAUDE.mdas a shim.
