# Restrict agents with path policies

> Read-only paths, writable paths, how the two compose by longest match, what agents are told, and what a refusal looks like.

Canonical URL: https://www.slivingdoc.dev/docs/guides/path-policies/ · Version: 0.1 · Updated: 2026-09-21

Full site index: https://www.slivingdoc.dev/llms.txt

Two flags decide which parts of the notebook one server process may
change. `--read-only-paths` names what it may never change.
`--writable-paths` names what it may change, and makes everything else
read-only for that process. Both take a comma-separated list of
notebook-relative paths, and both have an environment variable:
`SLIVINGDOC_READ_ONLY_PATHS` and `SLIVINGDOC_WRITABLE_PATHS`.

An entry protects or opens itself and everything below it, matched on
segment boundaries: `docs` covers a file named `docs` and every path
under `docs/`.

> **Warning:** This is a guardrail at the MCP tool boundary, not a
> security boundary against the agent. The `serve` process holds the S3
> credentials, and an agent that can read that environment, or launch its
> own slivingdoc process, bypasses the setting. It is the same model as
> an operator's sftp configuration: the policy lives in the server
> configuration, never in the data.

## Read-only paths

Use `--read-only-paths` to let a fleet of agents read injected material —
FAQ answers, reference documentation — without risking that one of them
overwrites it:

```text
export SLIVINGDOC_BUCKET=my-notes
slivingdoc serve --read-only-paths docs,faq.md
```

The restriction applies to the process configured with it, not to the
notebook. A human running without the flag keeps full write access to the
same paths:

```text
slivingdoc pull notes
# edit notes/docs/faq.md
slivingdoc commit notes -m "update the FAQ"
```

The next pull by any agent picks that change up with no conflict.

## Writable paths

Use `--writable-paths` to confine a fleet of agents to a directory each,
where listing what they must not touch is not possible — a directory that
did not exist at startup, or a file at the notebook root, would otherwise
stay writable:

```text
slivingdoc serve --writable-paths agents/scout
```

Because the writable set is non-empty, everything it does not cover is
protected for that process: other agents' directories, files at the
notebook root, and directories that do not exist yet.

## How the two compose

The two settings compose, and the longest matching entry wins, so a
protected region can hold a writable subdirectory:

```text
slivingdoc serve --read-only-paths docs --writable-paths docs/drafts
```

That process may write under `docs/drafts` and nowhere else.

Nesting can go deeper than one level, and the entries you wrote decide
there too:

```text
slivingdoc serve \
  --read-only-paths notes,notes/agent-a/locked --writable-paths notes/agent-a
```

That process may write under `notes/agent-a`, except under
`notes/agent-a/locked`, which the longer read-only entry protects again.
Listing the broader `notes` beside it changes nothing about the narrower
entry: adding an entry to a setting never makes a narrower entry of the
same setting stop applying.

A path named by both settings is a configuration error, not a silent
precedence rule. Startup refuses, naming the path and both settings.

## What agents are told

An agent learns the rule before it edits, and again if it forgets. With a
read-only set configured, the entries are named in the server
instructions, in both tool descriptions, and in the `readOnly` array of
every pull and commit result.

With both sets configured, the server instructions, both tool
descriptions, the result text item, and the report name both sets and end
with the rule that decides between them: where the two sets nest, the
longest matching entry decides. That replaces the plain "write elsewhere"
sentence, which a non-empty writable set would make false, so an agent is
never told to write only under an entry and, in the next sentence, that
changes under it are refused.

## What a refusal looks like

A commit that changes a protected path is refused, the touched files are
reset to the last accepted content, and the result names the violated
entries:

```text
INVALID_REQUEST · READ_ONLY_PATH
docs is read-only in this server. Your changes there were discarded and the files reset. Write outside the read-only paths, then commit again.
  docs/faq.md  read-only
next: edit the files, then commit
retryable: false
read-only: docs
```

The MCP structured result an agent decodes carries the same message plus
the stable `reason: "READ_ONLY_PATH"`, `action: "EDIT_FILES"`, a
`READ_ONLY` reason on the `docs/faq.md` file entry, and the `readOnly`
array naming every configured entry.

Under a writable set the refusal names where the process _may_ write
instead, because the protected region is then nearly the whole notebook.

A pull restores protected paths from the accepted remote state. That
restore applies to a workspace that passes the content rules: an invalid
file under a read-only path — a binary, a symbolic link, an invalid name
— is refused as `INVALID_CONTENT` naming that file, on pull and commit
alike, and the restore does not run until that file is deleted.

## Clearing an inherited value

Like every other shared flag, an explicitly empty flag clears an
inherited environment value instead of falling back to it:

```text
slivingdoc serve --writable-paths=
```

That is how a process asks not to be confined when
`SLIVINGDOC_WRITABLE_PATHS` is set in the environment it inherits.
`--read-only-paths=` does the same for
`SLIVINGDOC_READ_ONLY_PATHS`.

## Startup refusals

An invalid entry refuses startup before any native or network dependency
loads, so a mistake never reaches the bucket. An entry is invalid when it
is an absolute path, when it contains a `..` or `.git` segment, or when
it is over the length bound. The same point refuses a path named by both
settings, and an entry that sits below another entry of its own setting.

## Next

- [Errors](/docs/reference/errors/) — every code, reason, and action.
- [Configuration](/docs/reference/configuration/) — the two flags beside
  every other setting.
- [Share a directory with humans](/docs/guides/shared-directory/) — the
  other half of a shared notebook.
