Security - WizardingCode-io/shibaox GitHub Wiki

Security

Shibaox runs agents on your machine with your user's permissions. These are the rules it enforces, and what they are not.

What is enforced

  • A connector's env_keys and the ${NAME} placeholders of its headers are names of vault entries (UPPER_CASE): a value pasted there is refused by the schema, by POST /mcp (bad_key_name) and by the app's form, and the app never prints a key-like text as a badge. Listings show a key's last four characters only. A connector whose key is not in the vault is left out of the task with a note; the task goes on.
  • Tools by allowlist. A role runs only the programs listed in its tools:. The direct adapter splits the command into argv and runs it without a shell: nothing is expanded ($VAR, ~, globs), operators (; & | $ < >, backticks) are refused, and arguments that are absolute, start with ~, contain .. or name .git are refused. The child gets a scrubbed environment (PATH, HOME, LANG, TMPDIR, TERM). write_file refuses any path with a .git segment.
  • Files inside the workspace. Read and write tools, and Claude Code's file tools, are allowed only for paths that resolve, through symlinks, inside the task's working directory.
  • Network by host. web_fetch, WebFetch and WebSearch only for the hosts in permissions.network, redirects included.
  • Commands have a meaning, and the risky ones ask first. One policy classifies every command in both runtimes: push (git push), deploy (npm publish, vercel deploy, kubectl apply, terraform apply, docker push and the rest, see Claude Code runtime), execute (a shell or interpreter given inline code: sh -c, node -e, python -c; sudo; runners that download code: npx/bunx of a package not installed in the workspace, pnpm dlx, uvx, pipx run), network (curl, wget, http reaching a host outside permissions.network, or with no recognisable URL; ssh, scp, rsync to a remote). A categorised command is refused unless the role lists that category in permissions.approval_required and you approve that exact command (its environment prefixes included), in the inbox, the dashboard or Telegram; git status, gh pr view, kubectl get, npx tsc (installed, bare name) and every other plain call of a listed program run without asking. Deploy programs (vercel, fly, netlify, heroku, railway, wrangler, kubectl, terraform, helm) are read-only by allowlist: kubectl get|describe|logs, terraform plan|show, fly status|logs, helm list and the like run; anything else they do is a deploy (kubectl exec, heroku run, fly secrets set, terraform taint…). git follows one parser in both runtimes: no user aliases, no -c/--config-env, no GIT_* environment, no options that run other programs. Since 0.1.15 roles that list node, python3, bun, curl, npx and the like no longer run inline code or unlisted hosts silently in either runtime: add execute/network to approval_required to be asked instead (the scaffold's assistant role has execute), and approval_required accepts only the five categories (an org with another value fails to load with the file named).
  • Protected files. permissions.protected on the role and protected: in the project's shibaox.yaml are globs (relative to the workspace, ** allowed) that the file tools of both runtimes will not write: refused, or asked as a protected approval naming the path when the role lists protected in approval_required. Paths are judged as written and through symlinks and the filesystem's case, relative to the workspace; shibaox.yaml itself is always protected and is read from the project checkout, never from the run's worktree (an unparsable file fails the task rather than unprotecting it). Globs: dotfiles match (secrets/* covers secrets/.env), a pattern without / matches at any depth (*.lock covers a/yarn.lock), infra/ and /infra/** mean the directory. Shell commands are not inspected for the files they touch (sed -i on a protected file goes through): review the diff before it lands. An approval of a write covers that path for the rest of the node, whatever the content. gh runs under an allowlist: reads run freely, everything else (merges, reviews, comments, releases, repository settings, secrets, workflow runs, writing API calls) is a deploy that needs an approval, and extensions, aliases, auth, keys, gists and codespaces are refused (GitHub loop).
  • Routines run on the daemon's machine. A command trigger runs a shell command and a file trigger reads a file there, every interval, with the daemon's rights and the GitHub token: they are accepted from the socket or from org/routines files, never from a remote client; url triggers reach public addresses only. What a trigger saw is handed to the run as data. See Routines.
  • The setup step runs what the repository says. A worktree run installs dependencies first (shibaox.yaml setup, else the lockfile's frozen install) through a shell, as the daemon's user, with the GitHub token from the vault, before any model or person is involved; a package.json runs its own install scripts. Only run worktree runs on repositories you would npm install by hand, or set setup: off in org.yaml. See Worktrees.
  • Git nodes land only after a human node. The template puts approve-push before commit and merge; a cancelled run never lands.
  • Keys stay in Shibaox. The vault is a 0600 file in a 0700 directory; runtimes get only the keys they need; channels never receive file contents, diffs or tool output; the socket is 0600 and local. code nodes, gate commands (tests, lint, code) and git nodes run with the process environment plus a short list from the vault: GH_TOKEN, GITHUB_TOKEN, GH_HOST, the git author and committer variables and SSH_AUTH_SOCK. Never the provider keys: a test the model wrote runs there, and its output becomes evidence.
  • Files of a run are readable to the token, never secrets. GET /runs/:id/files lists what a run changed (its diff plus the files it reported) and GET /runs/:id/files/content?path= serves one file of the run's workspace: an in-place run's workspace is the project checkout, so a token holder can read any file the project has, as a shell could. Paths never leave the workspace (absolute paths, .. and symlinks out are refused), and .env, .env.*, .git/** and the project's protected: globs are refused with 403 whatever the run touched. Downloads stream the whole file; the view stops at 2 MB. PUT /runs/:id/files/content?path= (the app's "Save to project") writes a text file into the workspace under the same fence (403 outside it or protected, including through a symlink; 409 while a run is executing in that workspace, a paused one is not; 413 past the cap), recorded as a file_changed of node you: the token is shell access, so no approval is asked.
  • The app in the browser holds the token. shibaox app opens the browser app with the daemon token (or a per-session bridge token) in the URL fragment; the app keeps it in browser storage until Disconnect. The app's files are public on every listener; every API call is authenticated. Treat the printed address as the token (The app).
  • The network listener needs a token. With listen in daemon.yaml (or shibaox serve) the API answers on TCP only to requests with the bearer token, compared in constant time; without a token the daemon refuses to listen; a request with no token gets the version and nothing else.
  • Everything is on the record. Every tool call, approval and decision is an event in the run log, kept on disk across restarts; shibaox audit <runId> writes it out as a document (who approved what through which channel, every tool call with its duration, every gate with its evidence, what git did, the cost per node).

What is not

A daemon token is shell access on the machine that runs the daemon, as that user: the API takes any org and any project path, and tasks run programs. Keep it like an SSH key, send it over TLS, an SSH tunnel or a VPN only, and rotate it by changing the vault key and restarting serve. See Remote daemon.

POST /mcp/:id/test starts an org-defined command. Over the network it is allowed for the daemon's own org and the orgs of daemon.yaml projects only (403 for any other org path); on the socket, for any org, like everything else on the socket. Secrets from env_keys go to the server process's environment, never on a command line, in both runtimes.

The org is changed from the daemon's machine only. The Customize writes (POST/DELETE /skills, PUT /roles/:id, POST/DELETE /mcp) and PUT /orgs/config change org files that later run as commands and prompts, so they are refused with 403 to a network caller that is not on the daemon's machine, as the Higgsfield login is; the socket and a loopback client of the listener may make them, and reads answer anyone with the token. A reverse proxy on the same host would make every client look like loopback, so a request carrying X-Forwarded-For or Forwarded is never treated as local (403 for those writes, whatever the peer address); do not put a proxy that strips those headers in front of the listener. A skill installed from a repository is copied as plain files only: regular files of at most 1 MB each (bigger ones are left out and listed as omitted) and 10 MB per skill, never a symlink (a symlink in the repository is checked out as a plain file and skipped), never anything outside the skill's own directory, never .git. The clone is shallow, single-branch, without submodules and without a checkout at first: the tree of the path asked for is listed and refused (413) over 50 MB or 20 000 files before a single file is written, then only that path is checked out. Git runs without a shell, without the daemon's keys, with no user or system git config (GIT_CONFIG_GLOBAL=/dev/null, GIT_CONFIG_NOSYSTEM=1), no credential helper, no prompt, and https as the only protocol (plus file for a local caller's local repository); one clone runs at a time, capped at 60 s, in a temporary directory that is always removed, and it is killed when the daemon stops. A file:// or local repository and the folder source are for callers on the daemon's machine only. A literal secret in a credential header (Authorization, *token*, *key*) of POST /mcp is refused: keep it in the vault and write ${NAME}. A skill is still text the model reads as instructions on every task of the roles that list it: read a skill before giving it to a role, as you would a prompt.

GET /plugins shows account details. The Higgsfield row names the account the CLI is logged in as (its email, plan and credits) to anyone holding the daemon token, like the rest of the read API.

Telegram pairing writes daemon.yaml: local callers only. POST /plugins/telegram/actions/pair is refused to another machine and through a proxy (403, like the Higgsfield mode); it writes only channels.telegram.chat_id. The bot token stays in the daemon: the pairing, the test message and telegram_send call the Bot API from the daemon, and the token never reaches a model, a response, an event or a log line.

Higgsfield's API key stays in the daemon. HIGGSFIELD_API_KEY lives in the vault and is read by the daemon's own higgsfield_api_* tools at each call; it never reaches a subprocess (the command allowlist, the Claude Code runtime and the CLI probes get it no more than any other vault key), a model, a message, a log, an event or a result: errors and log lines are scrubbed of the key, its secret half and any URL with a query string. GET /integrations/higgsfield and GET /plugins say only whether a key is saved and whether Higgsfield accepted it: the daemon asks for the status of a request id that does not exist (401 = refused, 404 = accepted), at most once a minute per key, and caches the answer under a SHA-256 of the key. Signed upload URLs are never returned or logged; the upload PUT carries exactly the headers Higgsfield returned (plus the file's content-type when none of them is one) and never an Authorization header. The API base cannot be redirected through the vault: SHIBAOX_HIGGSFIELD_API_BASE is read only from the daemon's (or the CLI's) launch environment and must be https, or http on a loopback host; the vault refuses every SHIBAOX_* name (400, "reserved for Shibaox's own settings") except SHIBAOX_TELEGRAM_TOKEN and SHIBAOX_DAEMON_TOKEN. Generation results are public CDN URLs and appear in the run's events as tool results. PUT /integrations/higgsfield (the mode in daemon.yaml) is for callers on the daemon's machine only (403 otherwise, and with X-Forwarded-For/Forwarded). The higgsfield command stays in the assistant's tools in API mode: only the skill keeps the model from spending the account's credits there.

MCP tools are not approval-gated. A role that lists a catalog server under mcp: gets every tool of that server (or the tools: allowlist) without the push/deploy/network categories: Shibaox cannot tell what a foreign tool does. The Playwright server browses wherever the model decides. Give servers to roles you would trust with that, use tools: allowlists for servers that can act, and keep their secrets in env_keys (the vault), never in env.

An allowlist is not a sandbox, and execute only catches inline code: listing make, xargs, find (-exec), env, timeout, docker run, a package manager's run/scripts, or node script.js/sh script.sh on a script the model wrote grants full execution. An allowed program such as node, pnpm or make can run arbitrary code: a package.json script, node -e, a script the task wrote. Claude Code's Bash tool snapshots your shell rc files, so anything they export can reach the task's shell. Cancelling a run does not kill a command that is already running. Only list programs you would let the model run unattended, and run Shibaox on projects and machines where that is acceptable.

Model outputs (summaries, review verdicts, commit messages) are quoted as data in later prompts, but a model can be steered by what it reads in your repository or on the web. Keep permissions.network narrow and review what lands.

  • A catalog server may name a command that prints its bearer token (bearer_command, e.g. [higgsfield, auth, token]): the daemon runs it before each connection with a minimal environment (PATH with the user bins, HOME, USER, TMPDIR, LANG: never its keys), keeps the token as a secret of that connection (an environment variable for the Claude Code runtime, never on a command line or in the logs, events or audit) and skips the server with a note when the command fails, so a task never dies for a missing login. It is the same trust as a stdio server's command: org files are trusted configuration, like the commands a role may run.
  • download_file (the direct runtime, roles that write and have a network policy) fetches an https URL into the workspace under the same rules as write_file and web_fetch (allowed hosts, a protected path asks first), https on every redirect hop, up to 200 MB, and records a file_changed.
  • Attachments (POST /runs) are written under attachments/ of the run's workspace with the same fence as any write (base names only, never through a link, never a protected path), at most 20 files and 25 MB per message; the folder is added to the repository's local exclude file and skipped by git nodes and diffs.
⚠️ **GitHub.com Fallback** ⚠️