Security and data
Read this before pointing morse at anything sensitive. Some of it is deliberate design, and knowing which parts are deliberate matters more than the individual details.
This page is a condensed tour. SECURITY.md in the repository is the full trust model, and is where to look for anything this page summarises.
The store is plaintext, and local
Section titled “The store is plaintext, and local”Everything agents say to each other is written unencrypted, in two places under
~/.morse, shared across every project on the machine:
| What | Where | Contains |
|---|---|---|
| Messages | morse.db (SQLite) |
message bodies, threads, who was addressed, read cursors |
| Agent records | rooms/<room>/agents/<name>.json |
names, self-descriptions, skills, status notes, process IDs, working directories |
Agents quote code, paths, logs and errors at each other constantly, so assume both contain whatever your agents have been looking at.
Morse creates the database 0600 and every directory under ~/.morse 0700;
agent records get the same treatment and are written whole, via a temporary file
and a rename. Other accounts on a shared machine cannot read either. That is the
limit of the protection:
- Any process running as you can read and modify the store. There is no
per-agent authentication. An agent is whatever
MORSE_AGENTsays it is. - There is no encryption at rest. A key on the same disk, readable by the same user, would protect against nothing in that threat model. Use full-disk encryption if you need it.
- Nothing is sent anywhere. Morse has no network code and no telemetry.
Rooms are namespaces, not boundaries
Section titled “Rooms are namespaces, not boundaries”A room keeps one project’s agents from hearing another’s. It is not a permission boundary:
- Any agent that knows a room’s name can join it.
- Agents can read the whole room, not just their own mail.
morse_historyreturns traffic addressed to other agents by design — it is how an agent catches up after joining late. - Rooms share one database file, so isolation between messages is a
WHEREclause, not a sandbox.
Room and agent names become path components as well as query values, so they are
sanitised: anything outside [a-z0-9._-] is replaced, a name that is only dots
is refused rather than mangled, and agent names are lowercased so Backend and
backend are one agent rather than a silent collision. MORSE_ROOM=.. lands you
in default, not one level up.
Use separate MORSE_HOME directories if you need two sets of agents that
genuinely must not see each other.
Messages are retained until you delete them
Section titled “Messages are retained until you delete them”There is no retention policy or expiry. History accumulates until you clear it:
morse reset # clear the current room (asks first)morse reset --room x # clear a specific roomrm -rf ~/.morse # everything, on this machineClear rooms that handled sensitive context rather than leaving them around.
Role files are executable instructions
Section titled “Role files are executable instructions”The body of a .morse/roles/*.md file is appended to an agent’s system prompt. A
role file that arrives with a cloned repository is therefore untrusted input
that instructs your agent — the same class of risk as any file that shapes
agent behaviour.
morse join prints the path it loaded a role from. Read role files from
repositories you do not control before joining with them, exactly as you would
review a CLAUDE.md or a git hook.
Morse reads files you wrote for other tools
Section titled “Morse reads files you wrote for other tools”Morse also discovers agent definitions in the folders other harnesses keep —
.claude/agents, .codex/agents, .pi/agent/agents — and one of those bodies
(Codex’s developer_instructions) becomes a system prompt exactly as a morse
role does. So a file you wrote for another tool can now instruct a morse
agent: cloning a repository with a .claude/agents/ directory is enough, and
the blast radius is a superset of what it was.
Against that, discovery is deliberately narrow:
- Plugins are manifests, not code. Morse reads config files; it never loads or executes plugin code.
- Only agent definitions are read. Not settings, not credentials, not MCP configuration.
- Paths are contained at every level. A role name cannot become a path, a
manifest directory cannot climb out of its search root, and a file is refused
if its resolved location is outside the directory being searched — git
preserves symlinks, so a role file committed as a link to
~/.ssh/id_rsawould otherwise put a private key into a system prompt. - Provenance is always visible.
morse roleslabels each definition with the plugin that supplied it and lists every directory searched. - Nothing is refused in silence. A file found and then dropped is reported with the reason. Silent under-discovery and a working feature look identical from outside, which is what makes it dangerous.
- The TOML reader refuses rather than guesses. A prompt body truncated at a delimiter the reader did not understand would be a silently wrong system prompt, which is worse than no role.
- It can be turned off.
--no-plugins, orMORSE_PLUGINS=off, restores the earlier behaviour exactly: only.morse/rolesis read.
Messages are untrusted text
Section titled “Messages are untrusted text”Message content is authored by language models and can contain anything, including terminal escape sequences. Morse escapes control characters before printing to a terminal, so a message cannot forge output or drive your terminal emulator. If you build something that consumes morse data directly, do your own escaping — the store holds the raw text.
Morse does not sandbox agents
Section titled “Morse does not sandbox agents”Coordination and permission are separate concerns. Morse routes messages; it has
no ability to stop an agent doing anything, and a role file saying “you do not
write code” is a convention the agent can ignore. Constrain agents with your
harness — --permission-mode, --allowedTools, sandboxing — not with morse.