Privileges and security

On this page

Short Leash asks for administrator rights once, keeps everything on 127.0.0.1, and checks every request from the internet before it reaches a site.

No Short Leash code runs as root

Setup makes its privileged changes with one osascript … with administrator privileges call that installs two plain files: the launchd plist and /etc/resolver/test. Short Leash writes both files itself; the only values in them are your user name and Short Leash's paths, which are XML-escaped in the plist and quoted in the script, and the TLD is fixed. Trusting the certificate authority runs as you, in your login keychain, with macOS's own confirmation. launchd binds ports 80 and 443 as root and hands the sockets to leashd, which runs as you. Removal undoes the same two files with one more prompt.

Loopback only

The proxy, DNS server, services, workers, Mailpit and the dump collector all bind to 127.0.0.1. Site terminals listen on nothing: they're reached only through leashd's local socket, and their shells run as you. Nothing Short Leash runs is reachable from your network. Sharing works by cloudflared connecting out to Cloudflare, so no ports are opened.

Certificates

Short Leash's certificate authority is name-constrained to .test: even if its key leaked, it couldn't sign a certificate a browser would accept for any other domain. Site certificates last at most 397 days and renew automatically. The CA's key stays in Short Leash's folder, readable only by you.

Shared sites

Requests from Cloudflare arrive at a separate listener. Password and IP checks happen there, before your site sees the request. Visitor IPs come from Cloudflare's CF-Connecting-IP, which visitors can't set themselves.

Whether a site is shared or not, Short Leash never serves its dotfiles, such as .env or .git/config, as files, except under .well-known, and a static site never serves a file through a link that leads out of its folder, so a cloned site's link to ~/.ssh stays unreachable. See ADR 0002.

Webhook deliveries arrive at the same listener, and the IP allowlist applies to them too. The share password doesn't, because providers can't type one. Instead, each webhook checks its provider's signature, and with Reject deliveries that fail the signature check on it answers 401 before any site sees the delivery. Short Leash finds a webhook from the request's host: its public name on your domain, or an internal name that only its own quick tunnel sends. Forwarded deliveries reach your sites as local requests, so they never trust forwarded headers.

Downloads

Downloads come over HTTPS from each project's official releases. Where the project publishes a SHA-256, Short Leash checks the file against it and refuses a mismatch: Node against nodejs.org's SHASUMS256.txt, Composer against getcomposer.org's checksum, and GitHub-hosted downloads (cloudflared, Mailpit, PostgreSQL, Meilisearch, RustFS) against the digest GitHub reports for each release file. MariaDB on Windows comes from archive.mariadb.org, checked against the sha256sums.txt beside it; Short Leash refuses the download if that file can't be read. MongoDB comes from fastdl.mongodb.org, checked against the .sha256 beside each archive and against MongoDB's release signature, made with the release key for that version, which Short Leash carries; its Database Tools are checked against the SHA-256 in downloads.mongodb.org's list of their downloads. Short Leash's own builds (MariaDB on a Mac, PostGIS, pgvector, Memcached, PHP with more extensions and Ruby) come from a manifest signed with Short Leash's release key, and each file is checked against the SHA-256 in it. MySQL and the static PHP builds publish none, so they rely on HTTPS alone. Valkey and Redis are built on your computer from their release source, checked first against the SHA-256 in each project's list of release hashes; Short Leash refuses the download if the list can't be read or doesn't name it. Typesense's macOS build comes from dl.typesense.org, which publishes no checksums, so Short Leash carries each version's SHA-256, copied from Typesense's own Homebrew formulas, and refuses a download that doesn't match. Xdebug is built on your computer from its release source. Updates to Short Leash itself are checked against Short Leash's signing key.

Secrets

The share password and Cloudflare tokens are stored in secrets/sharing.json in Short Leash's folder, with permissions 0600 (on Windows, an access list for your account and SYSTEM alone, set on the secrets folder and inherited by what's in it). They're never shown back in the app or sent anywhere except Cloudflare's API. Webhook signing secrets are stored the same way in secrets/webhooks.json, and the secret values of calls' environments and the OAuth 2 tokens calls get in secrets/calls.json. The app only sees their last four characters. Calls' History and AI tools see •••••• in place of a secret value.

Calls sent by AI tools through leash mcp reach the site and its share URL only, unless the site's settings let AI tools call other hosts; even then, addresses on your computer and your local network are refused, on every redirect and after the name is looked up, so an agent can't use Short Leash to reach your services. Calls never run scripts from their files, so a collection cloned with a repository can't run code. Their YAML is read by one parser that checks it as it builds: a calls folder's files can't use anchors or aliases (&name, *name), which Short Leash never writes, or pass 4 MB each (64 MB together), 10,000 entries or 32 levels of nesting, and files you import may use anchors but their aliases can copy in no more than 200,000 entries and 32 MB, so a few KB of nested aliases can't fill memory. Everything an import keeps is held to 64 MB, measured as JSON and counted through one gate as each piece is added: its calls, folders, folder settings, environments (each Insomnia sub-environment's copy of its base included) and warnings, of which it keeps at most 100. Past that it stops, keeps what fits and says how many calls. Names, media types, summaries and descriptions are cut to short limits, a property name that isn't text is skipped rather than copied, and the examples an OpenAPI import builds from its schemas count against the same 64 MB and do at most 200,000 values of work. Calls lists each limit by format. See ADR 0050.

Every site name or id an AI tool puts in a request's path, through leash mcp or Short Leash's chat, is checked against the names Short Leash gives them before leashd is asked anything, and every other value is encoded, so a made-up argument such as shop/share? can't reach another of leashd's routes. See ADR 0062.

Process logs and notifications stay in memory. The Monitor keeps requests, what the app did for them and what the browser saw in its own database, db/monitor.db, with bodies over 64 KB in db/monitor-blobs/, for 7 days or 1 GB by default. Before anything is written, cookies, authorization headers, values whose names look secret (password, token, secret, key, code, session…) in query strings, forms, JSON and URLs, query values bound to such columns, and values that look like tokens become [masked]. Masking by name and shape can miss a secret written into free text, such as a dumped object. The live view keeps the last requests as they happened while Short Leash runs. Traffic through a share is kept with smaller bodies and within a fifth of the limit, and AI tools get its bodies only when they ask for them. See ADR 0052.

Webhook deliveries (headers and bodies included), task lists, terminal details and per-minute request counts are kept in Short Leash's database, db/leash.db. Only leashd opens it; the app, the leash command and AI tools go through leashd. The db/ folder can only be opened by you (0700), and the databases, their write-ahead logs, copies and the bodies kept beside them are readable only by you (0600). Secrets never go in it. Site terminals' output is saved under terminals/, also readable only by you, since a program may print anything. Request headers that usually carry secrets are hidden on screen until you choose to show them. See ADR 0029.

In a Pair call, nobody controls a screen until the person sharing it gives control: the host for their screen, or a guest for theirs (ADR 0046). A guest's computer gets the same protections as the host's: their own protected apps, no keys while macOS secure input is on or into administrator windows on Windows, a confirmation the first time, the frame while it lasts, and their own mouse or keyboard takes it back. Every message about control goes through the host's leashd, which accepts control of a guest's screen only from that guest during their turn. Asks to unmute, turn on a camera, share or give control never turn anything on by themselves, and the host keeps them to a few a minute. See ADR 0038.

Pair avatars travel from one computer to another, so Short Leash decodes each one, shrinks it and encodes it again as a small JPEG before anyone in the call sees it: your own when you choose it, a guest's when they join (on the host), and those the host passes on (on each guest). One that doesn't decode is dropped. Yours is kept in pair/avatar.jpg, readable only by you. See ADR 0044.

A project's own file

A site's leash.toml, and leash.local.toml beside it, come from the project's repository, so Short Leash treats them as untrusted. Reading them is safe: Short Leash reads each file only from the site's folder, without following a link, up to 64 KB, and the PHP and Node versions, document root, aliases and HTTPS it sets apply straight away, served on 127.0.0.1 like any site. An alias never takes another site's address or one of Short Leash's.

What runs needs trust. The file's processes, its services, and its env outside PHP's requests wait until a person trusts the folder in the app or at a terminal, never an AI tool or chat. The grant is for a fingerprint of what the files run, so Short Leash asks again when a command, a service or the env changes, and the processes the old file started stop at once. It's kept in trust.json in Short Leash's folder, readable only by you.

Neither file holds secrets. Values that look like one are ignored when the file is read and left out when leash init writes it. Names that change how programs load, such as LD_PRELOAD or any DYLD_ variable, and Short Leash's own names are refused. See Project file and ADR 0078.

Reading Git repositories

A site's Changes page and the Git line on its Overview run the Git you have installed, and treat the repository as untrusted. Each command that reads turns off what a repository's own settings could make Git run: an fsmonitor hook, the filters its own config defines, text conversions and external diff tools. It never takes Git's lock, never asks for a password, never reaches a remote (a partial clone doesn't download what it's missing, and no transport is allowed), and never looks for a repository in your home folder or above it. Git's own ownership check (safe.directory) is respected, never overridden. New files are read only when they're regular files, up to 1 MB. See ADR 0034.

Writing to Git, and hooks

Staging, discarding, committing and pushing go through a separate runner. A repository's hooks, and its own Git settings that run programs or change where Git connects, run only in a folder you trust through its project file's trust, never in one you haven't: there no hook runs at all, your own global ones included, and a repository with such settings can't be written to until you trust it. A hook or such setting that arrives after you trust it asks again. Only a person trusts a folder, in the app or at a terminal (leash changes commit --trust and leash workspace merge --hooks trust show what it covers, ask, and refuse without a terminal); no AI tool or chat can. A branch workspace's checkout and merge follow the same rules (ADR 0082). Transports that run programs stay closed even when trusted, pushes are never forced, credentials are never asked for or seen, and a discard leaves a copy in the Trash. See ADR 0081.

Pull requests through your gh

Reading a branch's GitHub pull request is off until you turn on Let chat read pull requests from GitHub. Then Short Leash runs your own gh, read-only (gh pr view and gh pr diff), outside the repository, for github.com only, with an environment built from an allowlist, so no token in Short Leash's environment reaches it. Short Leash never reads gh's files or handles a token. The answer is outside text, kept in memory for 60 seconds, and it's the one read that takes chat online. See ADR 0084.

Sites on a port Short Leash didn't start

A site can forward to an app you run yourself. Short Leash only ever connects to 127.0.0.1 or ::1 for it, refuses its own ports so requests can't loop, and never starts or stops that app. See ADR 0074.

Importing from another tool

Short Leash reads another local development tool's files only when you start an import, and never changes, stops or writes to that tool. It reads the tool's settings file, the names and targets of its links (never followed to open a file), its per-site web server files for PHP and HTTPS, and, when you convert project files, a project's own settings file, each up to 64 KB and never through a link. It never opens the tool's services folder, private keys, certificate authority, .env files or databases. Nothing changes until you choose Import, which reads the settings again and leaves out anything that changed since you reviewed it. A database comes across only as a dump you make yourself, restored into a new database: as in every restore, a MySQL or MariaDB dump that holds a client command or reaches another database is refused before anything starts. See ADR 0079 and Import from another tool.

PhpStorm's breakpoints

With Xdebug on, and the site's Debug when PhpStorm has breakpoints on, Short Leash reads one file of another app's: the project's .idea/workspace.xml, to count PhpStorm's breakpoints. It reads it only from .idea at the site's root, never through a link, up to 8 MB, without expanding entities, and only while the site has had a request in the last 10 minutes. It never writes it. This and an import you start are the only times Short Leash reads another app's files; see ADR 0079.

Text the local model reads

Chat, Explain, Review, meeting summaries and the OpenAI-compatible endpoint send the model text Short Leash didn't write: logs, mail, requests, diffs, comments, what people said. The model helper renders each chat with the model's own template and reads the result with its special tokens, the markers that open and close a turn (<|im_start|>, <|im_end|>) or hold thinking and tool calls. So before the template runs, the helper breaks every special token's text inside the messages with a zero-width space (leash_llm::Specials): in values and keys, at the end of a string where the template's next characters could complete one, and inside a tool call's arguments once their JSON is read. A tool result that writes <|im_end|> and a new assistant turn stays text inside its own turn; only the template writes real turns. See ADR 0062.