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.
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
serveprocess 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:
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:
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:
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:
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:
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:
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:
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 — every code, reason, and action.
- Configuration — the two flags beside every other setting.
- Share a directory with humans — the other half of a shared notebook.