Pipe

Security

Pipe v0.50.5

Model

Pipe gives a language model access to a server, so the security rules live in code, not in the prompt. The model can make a mistake or be manipulated by the content of a file it reads (prompt injection); the classifier, confirmations, secret redaction and worker limits work regardless of what the model "wants" to do.

LayerWhat it does
Command classifiersafe / confirm / forbidden, fail-closed
Autonomyruns routine work itself through the safety fuse, asks about important things (core/autonomy.py)
Confirmationshows exactly the string that will run
Secret redactiontool results are cleaned of keys and passwords before they are sent to the LLM
Workers and routinesrun only safe; everything else comes back as a proposal
Workspacefile tools see only the host and can't see SSH keys, /etc/shadow, /proc...
Audit logevery operation, without file contents
TokenAGENT_TOKEN required for every request, including confirmations and subscriptions

Command classification (backend/core/security.py)

Order:

  1. FORBIDDEN on the whole string, which catches curl ... | bash and $(rm -rf ~).
  2. Splitting into segments on ;, |, ||, &&, & and newlines, respecting quotes (grep 'a|b' is one segment).
  3. Each segment: forbidden → sensitive files (confirm) → confirm patterns → recognised read (safe). Matching is token-based: ss doesn't match ssh, ps doesn't match psql, id doesn't match idiotic-cmd.
  4. A write redirect (>, >> other than /dev/null and 2>&1) or a dynamic construct ($(...), ${...}, $'...', backtick, <(...)) → confirm, even when the segments are reads.
  5. Anything the classifier doesn't recognise → confirm (fail-closed).

SAFE – recognised reads

Files and text (cat, grep, ls, find, du, df, jq...), system (ps, free, uptime, systemctl status, journalctl), network (ss, ip addr, ping, dig, curl GET), Docker (ps, logs, inspect, stats, compose ps/logs/config/top, exec with a recognised read inside – also sh -c '...'), Git (status, log, diff, show, listing branches/tags), Kubernetes (get, describe, logs, top, rollout status), Helm (list, status, get), dmesg (without clearing), sed without -i and without the e/w commands, awk without system(), pipes, getline, print > file and ENVIRON.

Arguments that change state are checked separately:

CommandRequires confirmation when...
gitthe subcommand isn't a read, a flag runs a program (-c core.pager=, --upload-pack, --receive-pack, --exec, -O/--open-files-in-pager) or writes a file (-o/--output), branch -D, branch new, tag v1, remote add, config without --get/--list
find-exec, -execdir, -ok, -delete, -fprint
curl-o/-O, -T, -d/--data, -F, --json, -D, --trace*, -X/--request (also --request=, -X<METHOD>) other than GET/HEAD, or a host other than localhost/private (exfiltration), including proxies (-x, --proxy, --socks5), --resolve/--connect-to and an address written as a single number (http://134744072/)
sort-o/--output, --compress-program (runs a program)
journalctl--vacuum-*, --rotate, --flush
kubectl get secret-o yaml/json/jsonpath/go-template/custom-columns (also -o=...) or --template, also when secrets are in a resource list (get cm,secret), because the secret's content would reach the LLM

Reading these paths also requires approval, because it would send secrets to the provider: /etc/shadow, SSH keys, *.pem/*.key, .env (also relative cat .env), /proc/*/environ and /proc/*/root (any PID, self and globs; with pid: host these are the environment and home directories of host processes, a way around the read-only /hostfs), and globs in secret directories (cat /etc/s*adow, cat /root/.ssh/id_*). Patterns are also checked after removing quotes and backslashes, so cat /etc/sha""dow won't get through. The same files read with read_file also require approval.

CONFIRM – state changes

Not all of them wait for you – Pipe runs routine work itself (the Autonomy section below). The classifier sends these to confirmation: rm, mv, cp, chmod, chown, kill, systemctl start/stop/restart/enable..., apt install/upgrade/remove, docker run/exec/start/stop/restart/rm/compose up/down, git push/commit/pull/reset..., kubectl apply/delete/scale/exec..., helm install/upgrade/uninstall, ufw, iptables (except listing), reboot, crontab (except -l), and every unknown command. Reading files with secrets (/etc/shadow, SSH keys, *.pem, *.key, .env) also requires approval.

FORBIDDEN – always refused

rm -rf / (and variants: -fr, /*, ~, "/", /hostfs), rm --no-preserve-root, dd if=, mkfs, wipefs, shred, writing to /etc/passwd, /etc/shadow, /boot/, /dev/sd*, fork bomb, curl|wget ... | sh, base64 ... | sh, python -c ...exec, kubectl delete ns kube-system. A refusal doesn't go back to the model as an error to retry; it gets a message telling it not to try to get around it.

Regression tests for bypasses found during the rebuild: backend/tests/test_security_hardening.py, backend/tests/test_review_fixes_v091.py.

Besides the hand-written cases the classifier is fuzzed (backend/tests/test_security_fuzz.py, hypothesis): a recognised read plus an appended command (;, &&, |, &, a newline, $(...), backticks, <(...), ${IFS}, $'\x3b'), a redirect to a file, a command name split by quotes or a backslash, case and whitespace changes, reading a secret with a quoted path. There is one property: none of it may be safe, and no text may crash the classifier. CI runs 300 examples per test; deeper locally: PIPE_FUZZ_EXAMPLES=5000 pytest backend/tests/test_security_fuzz.py. Counterexamples go to the KNOWN_BYPASSES list as permanent regressions.


Secret redaction

Every tool result (commands, files, worker reports) goes through memory.redact_secrets() before it is sent to the LLM provider:

The agent can't write content containing the [ZREDAGOWANO marker with write_file; otherwise rewriting an .env file would destroy the real values. It has to change such a file in place (for example sed -i, with confirmation). Turning it off: REDACT_SECRETS=0 (not recommended).

Results longer than 24,000 characters are truncated (beginning + end), and the session history is trimmed at a user message boundary, so a tool call / result pair is never split.


Confirmations: what you see is what runs

Autonomy: what Pipe does without asking

An operation the classifier leaves for confirmation also goes through backend/core/autonomy.py before it reaches you. Routine work runs right away, through the same safety fuse as after YES: file backup to the journal, a check before (nginx -t, sshd -t, docker compose config), verification after (service active, container running, sites responding), automatic config restore, an entry that /undo reverts. Routine means:

Pipe always asks about important things, with the reason in code:

ReasonExamples
data it cannot back uprm -r, globs, files larger than the journal backup (5 MB), docker rm, volumes, system prune, compose down -v, apt remove, git reset --hard, git clean, git checkout -- ., kubectl delete
access to the serverSSH (/etc/ssh, .ssh/), users and passwords, sudo, PAM, firewall (ufw, iptables, nft), setuid, chmod 777
autostartcron, systemd units, .bashrc/.profile, /etc/profile.d, rc.local, logrotate.d, git hooks
the system and its sources/usr, /boot, /etc/apt, /etc/docker, fstab, sysctl, ld.so.preload, systemctl mask
codescript and program files (.php, .py, .sh, .js, .htaccess...), compose files and Dockerfile (compose up -d is routine, so they decide what runs), pip/npm install, docker run/exec, kubectl apply, kubectl set image, git pull <url>, bash script.sh, apt install ./file.deb, apt -o Dpkg::Pre-Invoke
a container with host accessdocker run with --privileged, docker.sock, --cap-add, --network host, a / mount
a site directoryfiles in /var/www, /srv/www, public_html, public/, static/, uploads/ – whatever lands there is visible from the internet
exposing directoriesweb server config with a new root, alias, autoindex; a config file copied from outside the server directory or content from another file (cat x > /etc/nginx/...)
outgoing datacurl/wget to foreign hosts, git push, scp, rsync, nc
cutting access or Pipestopping ssh, docker, the network; Pipe's own container and compose project; nginx -s stop; kill
machine restartreboot, shutdown
secrets and Pipe data.env, keys, tokens.json, Pipe's data directory
remote serverschanging files on an ssh/docker/kubectl target – there is no backup on the Pipe side
unknownsa command Pipe does not recognise, $(...) substitution, variables before the command (LD_PRELOAD=)

The question also comes back for every change in a turn in which the model read a web page, an MCP server result or an attachment (as with YOLO), for operations the model asked about itself (requires_confirmation: true), for writes to Pipe's memory (skills, SERVER.md), new targets, routines and MCP servers, and always for the viewer role and in observe mode. A rule error means asking, never running.

Why an unknown command always asks: Pipe reads logs, pages and files in which a stranger can leave text pretending to be an instruction. Without asking it runs only what it recognises and what the safety fuse can check or undo – so even a manipulated model cannot give itself access, add autostart or send data out. The worst it can do without you is a routine change with a backup in the journal.

Every such change is visible: the conversation shows "Without asking (routine, through the safety fuse): ...", the command's output appears under it marked without asking, and in the journal and audit log the interface has an [auto] suffix (e.g. telegram:123 [auto]). host: true commands in docker mode get the full fuse plan (backups from the host directory, checks and inverse commands through the helper container). AUTONOMY=0 in backend/.env brings back the question for every change.

YOLO mode (/yolo)

By default routine work runs by itself and important things wait for YES. /yolo on (CLI, pipe web, Telegram) turns that question off, only in this conversation (session), only for an admin and only until the backend restarts or /yolo off. What doesn't change:

Running without asking is visible: the client gets a progress "YOLO — running without asking: ...", pipe web shows a red badge above the input box, the CLI shows YOLO in the prompt, and the audit log and journal record the interface with a [yolo] suffix (for example cli:kuba [yolo]). The model gets a block about YOLO mode in its prompt (caution, the smallest change, no irreversible operations without an explicit request). YOLO takes the human out of the loop, so turn it on for a specific piece of work.

Observe mode (PIPE_OBSERVE=1)

To start with, before you trust the agent, you can install it so that it changes nothing: sudo bash scripts/install-server.sh --profile observe. Pipe still watches the server, sends alerts and reports, answers questions, draws diagrams and works as a read-only MCP gateway. Changes are refused on two levels.

In the application (runtime.observe(), native mode included):

In the container (backend/docker-compose.observe.yml, docker mode only):

backend/data (Pipe's memory) and the host's /tmp (the socket for the Telegram bot) stay writable. In native mode only the application-level protection applies: the process runs as root. Full mode comes back with sudo bash scripts/install-server.sh --profile full.

Internet (web_search, web_fetch, software_info)

The agent decides by itself when to go to the web, so the internet is treated as an untrusted source:

Attachments (files, screenshots, voice messages)

A file sent by the user is foreign text too: a log can contain a line written by an attacker:

Roles and tokens

Pipe on the internet (/web)

/web on exposes pipe web at a public HTTPS address (core/webpublic.py, core/webaccess.py):

A page on the internet is a new way into Pipe, which has docker.sock. Let in only devices whose code you see on your own screen, and revoke the ones you do not use. View only is enough for reading.

MCP

Pipe as an MCP server. An external agent gets Pipe's tools instead of a shell: every command goes through the classifier, a read runs right away, a change waits for admin approval (Telegram/CLI) and then goes through the safety fuse and the journal. Forbidden commands are refused, files with secrets are not handed out, results are redacted. A viewer token doesn't create approvals. Only the agent that created an approval can check it; approvals live in memory and expire after 30 minutes. ask_pipe runs the Pipe agent in the viewer role. HTTP endpoint: localhost only, a Pipe token (Authorization: Bearer), Origin validation (403) and 2026-07-28 header validation (-32020).

Pipe as an MCP client. In v0.35 the client was removed: another party's server ran as a Pipe process, with its access to docker.sock and files. Since v0.42 it is back with a different boundary: a stdio server (npx, uvx, a Docker image) runs in a one-off container without mounts, docker.sock or privileges (--cap-drop ALL, no-new-privileges, user nobody, limits), so at most it can use the network and change its own service (e.g. a GitHub repository) – not the server. Programs other than npx/uvx/an image and docker run options such as -v, --network host, --privileged are refused. Secrets only as ${MCP_*} from backend/.env or from mcp_secrets.json (0600; entered in the /mcp window in pipe web and the CLI through the mcp_servers klucz command, admin only, the value never goes back to a client and never reaches the chat or the audit log – only the name) – the model cannot point at ${LLM_API_KEY} or a Pipe token; text that looks like a secret in the configuration – refused. Catalog add-ons (backend/addons/) go through the same validation; state: true gives the server a named volume pipe-mcp-state-<name> at /state (never a host directory; removed together with the server), and catalog autoApprove covers only the login and the login state. Adding a server: one YES with a preview (or an admin's /mcp add). A tool without readOnlyHint – a YES on every call; annotations are the server's own declaration, so reads without asking are only as safe as your trust in that server (askAlways: true in the configuration turns this off for it). The result is treated like internet content (YOLO off until the next message), an argument with a secret does not go out. Observe mode and viewer: reads only. Every call is in the audit log.

Webhooks

Change journal and undo

Workers, targets and routines

Watching

The checks are deterministic and only read. The Investigate button sends the agent a message built by the backend (not by the client), and the usual rules apply from there. The event subscription requires the token, and the bot sends alerts only to users in TELEGRAM_ALLOWED_USER_IDS.


Workspace (file tools)

read_file, write_file, change_directory resolve the path (symlinks, ..) and refuse the paths below. In Docker a host symlink (/etc/nginx/sites-enabled/x -> /etc/nginx/sites-available/x) is resolved relative to the host root (/hostfs), and .. doesn't go above that root. Refused: everything outside the host (/hostfs in Docker), /boot, /dev, /proc, /sys, /var/spool, binary directories, /root/.ssh, /home/*/.ssh, /etc/shadow, /etc/gshadow, the SSH host private keys. Writing to /etc/passwd, /etc/shadow, /boot, /dev is forbidden; every other write requires confirmation.

The workshop (project) and Pipe writing first

The workshop (WORKSPACE_DIR, /root/pipe-workspace by default) is the only place where Pipe changes things without asking. The limits:

Messages Pipe sends by itself (the inbox, core/feed.py) carry buttons built by the backend; the client sends back only the message and action number (feed_act), so it cannot pass a command of its own. Actions that change the server (Free up space, Patch, Fix it) are for admins only. Fix it sends the proposal from the investigation to the agent as an ordinary message – the changes go through the classifier and confirmation as always. The investigation itself is a read-only worker, at most AUTO_INVESTIGATE_DAILY a day.

Agent memory

Memory goes into the prompt of EVERY future conversation (SERVER.md, DIRECTORY, VIBE) or is loaded on demand (skills), so one successful prompt injection could plant a persistent instruction there. Defences:

Skills from other repositories (/download, skill_manage install) are the same vector: before installing you see a preview (names, descriptions, source, what is skipped) and have to click Install or give a YES; only the SKILL.md text goes into memory, with its source added, not scripts or plugin files (hooks, commands, agents and programs – never). The repository is cloned like in the workshop (no hooks, submodules, LFS, a size limit) and removed after reading; its content is data – YOLO is off until the next message. Install only from sources you trust.

Still, review backend/data/ from time to time (/server, /directory, /vibe, /skills); the "data, not instructions" framing is not a guarantee.


Limitations you need to know about

Audit log

[2026-09-24 14:23:11] [cli:kuba] [SAFE] df -h /hostfs → exit_code=0
[2026-09-24 14:25:03] [telegram:123456789] [CONFIRMED] docker restart shop-web → exit_code=0
[2026-09-24 14:26:44] [telegram:123456789] [BLOCKED] execute_command(rm -rf /) → FORBIDDEN
[2026-09-24 14:30:02] [worker:web-1@telegram:123456789] [SAFE] ssh ... web-1 -- 'uptime' → exit_code=0

File contents (read_file, write_file, memory) never go into the log. Newlines in a command are escaped (\n), so a command with \n can't forge further entries. The token is compared in constant time (hmac.compare_digest).

Recommendations

  1. AGENT_TOKEN – the wizard and scripts/install-server.sh generate it automatically; in kubernetes mode it is required (the backend won't start without it). You set it by hand only to change it.
  2. Don't publish port 7379 on a public interface; access goes through an SSH tunnel or kubectl port-forward. The Kubernetes overlay adds a NetworkPolicy (deny from other pods) next to the token.
  3. Keep TELEGRAM_ALLOWED_USER_IDS to a minimum.
  4. For workers on other servers use a dedicated SSH key and a user with limited permissions.
  5. Read commands before confirming and review the audit log (/history) and memory (/skills, /server).

LLM provider keys added from the interface

pipe web lets you add an API key for another provider. The key goes from the browser to the local bridge (127.0.0.1), through the SSH tunnel to the backend, and stays there in DATA_DIR/llm_keys.json (0600). It doesn't go back to the client (the provider list only says whether a key exists and where it came from), it doesn't go into the audit log (only the provider change is logged) and not to the model. Before it is saved, the backend checks it with the provider, only at the address from the preset or the providers file, so the key can't be sent to an arbitrary address. Only an admin can make changes. The agent reading llm_keys.json is classified like .env (requires confirmation), and secret redaction covers tool results as usual.

Updating Pipe itself (pipe_update)

This tool pulls code from the network and runs it with the agent's permissions, so it is deliberately narrow:

Trust moves to the remote repository: whoever can push a commit to the tracked branch will run code on the server at the next update. It is the same boundary as with a manual git pull, but worth keeping in mind.

Edit this page on GitHub