# What a configuration can do

A `shotlist.config.yaml` is a file in a repository, and a repository is not always yours. This page describes what a configuration is allowed to do to the machine that runs it, and why the limits are drawn where they are.

## Two different questions

Limits in shotlist answer one of two questions, and it is worth keeping them apart.

**Is this a mistake?** Some limits exist because the behavior they prevent is almost never intentional, even from a configuration you wrote yourself. These cannot be turned off, because there is nothing to gain by turning them off.

**Do I trust the author?** Other limits exist because the configuration came from somebody else. These are off by default, since most configurations are written by the person running them, and are switched on with `--untrusted`.

## Limits that always hold

### Every network destination is approved

shotlist checks the protocol, host and port of every browser request, redirect and WebSocket, along with hosted fonts and the HTTP or TCP probe that waits for `site.serve`. A trusted Project approves the destination in `site.url` and any destinations in `site.allow`. Everything else is blocked before shotlist writes an Output image, because checking only the page's first URL would leave its subresources and redirects free to reach somewhere else.

A bare approval such as `assets.example.com` means HTTPS port 443. HTTP and unusual ports are explicit, as in `http://localhost:4321`, and `*.example.com` covers proper HTTPS subdomains rather than the apex. The exact destination is the unit because `http://example.com` and `https://example.com` do not offer the same transport.

### Some files are never touched

shotlist will not read or write a path passing through `.env`, `.git`, `.ssh`, `.aws`, `.npmrc`, private keys, keystores, or the other names listed in the reference. This is not about trust: a configuration you wrote has no reason to read your keys either, and a typo in an install destination should not be able to write into `.git`.

### Why that includes `.env`, and what follows from it

`.env` is on that list, which has a consequence worth stating plainly: shotlist will not load your `.env` file, and there is no option asking it to. A tool that auto-loaded `.env` would be opening the exact file it promises never to touch — and once it can open that file, a configuration can aim a `fontUrl` or an install destination at one.

That looks like it conflicts with reading secrets from the environment. It does not, because the two rules divide on who is asking:

- **A configuration must never make the tool read your secrets file.** That holds in every mode, and cannot be switched off.
- **The operator's own environment belongs to the operator.** If your shell exported a variable before shotlist started, shotlist reads it, the same way `echo` would.

So a `.env` file works fine — something else has to put it into the environment first, with `node --env-file` or `set -a`. See [Capture a page behind a sign-in](/docs/how-to/sign-in/).

This is the same line every limit on this page draws: shotlist confines the configuration, never the person running the command.

### Nothing evaluates

A configuration cannot run code. Queries travel into the page as data, so a malformed selector is a parse error rather than an execution, and `site.serve` runs a program directly with no shell. See [Why recipes cannot run code](/docs/explanation/no-code/).

### Work is bounded

Every Run limits document bytes, authored and expanded Step structure, Macro depth, executed Steps, `each` items, matching patterns and elapsed Recipe work. Lint rejects excess, and `--lint --warnings` reports use at 80%, so predictable excess stops before the site or browser starts rather than consuming the runner first. The Operator can raise a numerical limit with `--work-limit` or `SHOTLIST_WORK_LIMITS`; the Project cannot raise its own allowance.

## What `--untrusted` takes away

| Capability | Default | With --untrusted |
| --- | --- | --- |
| `site.serve` starts a process | yes | **refused** |
| Uses Project network approvals | yes | **none** |
| Uses Operator network approvals | yes | yes |
| Navigates an Application Recipe to `file:` | **refused** | **refused** |
| Reads or writes outside the project | yes | **refused** |
| `site.allow` approves destinations | yes | **ignored** |
| `allowEnv` names readable variables | yes | **ignored** |
| Loads a session | yes | **refused** |
| Reads `${env.NAME}` | named ones | **none** |

### Why `site.allow` is ignored rather than restricted

`site.allow` is a claim made by the thing you have decided not to trust. Honoring it in an untrusted run would mean the configuration could grant itself the permission the flag exists to withhold. `site.url` is ignored for the same reason. What the operator typed on the command line still counts and outlives the flag, which is why `--allow` and `--allow-path` exist and cannot be set from a configuration file.

The principle behind that: a control the configuration can switch off is not a control.

### Why the environment list is emptied rather than narrowed

An untrusted run resolves no `${env.NAME}` at all, even names that were allowed. There is no variable worth handing to a configuration nobody vouched for, and a partial grant is worse than none because it reads like a safe arrangement.

### Why sessions are refused

A session is a credential: the browser carrying it is signed in as whoever created it. A configuration nobody vouched for asking to be somebody is close to the whole reason the flag exists.

### What a session file holds

The cookies and local storage for the host in `site.url`, and nothing else by default. A sign-in that goes through an identity provider collects that provider's session on the way — one round trip through Google leaves cookies for `google.com`, `accounts.google.com` and `youtube.com` in the browser — and none of it is written. Those cookies are the person's account rather than a shot list's credential: they outlive the password if it is rotated, and no shot needs them.

`--login` names what it left out, and when it left anything out it loads what remains into a fresh browser and checks the session's `verify` selector against it before writing. An application whose session lives on a host that was dropped fails that check, which turns a run of screenshots of the sign-in form into one error naming the host. A file written by an earlier version is narrowed as it is loaded, so cookies collected before this never reach a browser again.

### What `keep` costs

For the application that genuinely needs a provider's cookies, a session may name the hosts to hold on to. The file then signs in as whoever those hosts know you as, and `--login` says so every time it writes one. Two things follow, and they are why it is not the default.

`keep` controls credential retention rather than network access. Reaching the provider still needs a separate destination approval in `site.allow` or from the Operator. A configuration holding both approvals can photograph a page of that account, and `--install` writes the image into the project. Without `keep` there are no such cookies to carry, which is why network permission alone does not expose the provider's session.

It is the configuration widening its own reach, so it goes the way `site.allow` goes: an `--untrusted` run is refused a session before `keep` is read at all.

### What protects the file itself

shotlist writes a session `0600` rather than at the default umask, which would leave it `0644` and readable by every other account on the machine. A mode only protects the file where it sits, and the cookies inside outlive the password if that is rotated — so a copy in a backup, on a shared volume or in a commit is signed in as that account until they expire. `.gitignore` is what covers the last of those.

## Three places can forbid a path

They are deliberately not equivalent:

- `deny` in the configuration is the project's own declaration, editable by anyone who can edit the file
- `--deny` belongs to whoever runs the command
- `SHOTLIST_DENY` belongs to whoever built the machine or the CI image

All three add up, and none of them can subtract. Only the last cannot be edited away by somebody writing a recipe.

## Variable names work the same way, in reverse

Three places grant a recipe a variable, and they add up: `allowEnv` in the configuration, `--allow-env` on the command line, and `SHOTLIST_ENV` in the environment. `SHOTLIST_ENV_DENY` is subtracted from the result, last and unconditionally, so an administrator building a CI image can put a name out of reach and no configuration key or flag adds it back. It takes globs, so `AWS_*` and `*_TOKEN` both work.

Two Project grants compose. `allowEnv` names a variable, `site.allow` names a destination, and a reference in a Step is substituted before the URL is checked — so a `goto` carrying `${env.TOKEN}` in its query string leaves with the value on it. Neither key is the hole; having both is. An `--untrusted` Run drops both Project grants together rather than letting the Project grant itself either half.

It is a fence rather than a boundary, and worth being precise about why. It stops `--allow-env AWS_SECRET_KEY`. It does nothing about `echo $AWS_SECRET_KEY` on the same machine, because the shell of whoever runs a command was never something a screenshot tool could stand in front of. It earns its place where shotlist is close to the only thing that runs — a container built to shoot configurations that came from somewhere else — and earns nothing as a control over a person at a terminal.

## What none of this covers

A destination approval reads the name from the URL. It does not pin that name to an IP address, so DNS rebinding can make an approved name resolve somewhere the approval did not intend. The policy is a fence rather than a network boundary.

shotlist has no database and issues no SQL, so there is nothing in it to inject into. It does have a browser, and a recipe is a script that types into whatever it is pointed at. A hosted instance that lets a stranger choose both the target and the keystrokes is a proxy for anything a browser can do to that target. The site scope is what keeps a submitted recipe aimed at the site it arrived with.

If you take screenshots of what strangers submit, put the runner where it cannot reach anything it should not, and rate-limit it. A run is one browser and amplifies nothing, but an open one is still traffic you are sending.

Everything above runs outward — a configuration reaching a site, a runner reaching a target. One direction runs the other way, and no flag closes it.

### Running `--login` on a configuration you did not write

`--untrusted` answers a configuration that is not yours everywhere except here: it refuses sessions and Project destinations, and `--login` is the command that makes a session. There is no mode in which signing in against a configuration you have not read is safe, and it is the command most likely to be run on a repository cloned five minutes ago, because it is what a project's README tells a new contributor to run first.

Two things a trusted configuration decides on your behalf. `site.url` approves and opens its exact destination, so a host resembling yours still gets a browser opened at it, with you at the keyboard. And `allowEnv` grants variables to `${env.NAME}` without `--allow-env` being typed by anyone, which a `--using` macro then fills into whatever `site.url` named.

Both are made visible rather than prevented, because a shot list has to be able to say where its own site is. Signing in by hand prints the URL before it waits for you. Signing in with a macro prints the host and the variable names before the browser starts, since that run is headless and there is nothing else to look at. Read `site.url`, `site.allow`, `allowEnv` and the sign-in macro before running `--login` on somebody else's configuration — four short keys, and the whole of what the command does with your credentials.

## Related

- [Command line reference](/docs/reference/cli/)
- [Configuration reference: `deny`](/docs/reference/configuration/#deny)
- [Run shotlist in CI](/docs/how-to/run-in-ci/)

Written by Nicola Mustone · Applies to shotlist 0.6.0 · Maintained by Nicola Mustone

Published date unavailable · Updated date unavailable · [View source](https://github.com/SirDarcanos/shotlist.dev/blob/main/src/pages/docs/explanation/security-model.astro) · [Propose a correction](https://github.com/SirDarcanos/shotlist.dev/edit/main/src/pages/docs/explanation/security-model.astro)
