MAE is in early alpha. Only the latest release on main receives security fixes.
Report security issues via GitHub Issues with the security label, or email the maintainer directly if the issue is sensitive.
For sensitive reports, include:
- Description of the vulnerability
- Steps to reproduce
- Impact assessment
MAE has several security-relevant subsystems. This section documents the current posture honestly — what's strong, what's moderate, and what's known-limited.
Permission tiers — The AI agent operates under a configurable permission tier:
- readonly — AI can read buffers and navigate, but cannot modify files (default)
- write — AI can edit buffers and create files
- shell — AI can execute shell commands
- privileged — Full access including configuration changes
The tier is an auto-approval ceiling, not a wall. Since ADR-090 a permission check answers one of three things:
| Answer | When | What happens |
|---|---|---|
| allow | at or below auto_approve_tier |
runs, no prompt |
| ask | above auto_approve_tier |
an interactive surface prompts a human; a non-interactive one denies |
| deny | a session-declared ceiling (ADR-051), a tool-category restriction (ADR-085), or a configuration that would not parse | refused outright; no prompt can raise it |
The default changed in v0.15 and this is a breaking change. MAE used to ship
auto_approve_tier = "trusted" (= shell), which auto-approved essentially everything. It now ships
readonly: reads run, writes and shell are asked. Nothing is silently denied, so the stricter
default does not break run_build/run_test — it asks about them. If you want the old behaviour,
set auto_approve_tier = "shell" explicitly and understand what you are granting.
Which surfaces can ask:
| Surface | ask |
|---|---|
mae-agent TUI (the default AI surface, ADR-049) |
prompts inline (y / a / n) |
Embedded editor session (:ai, delegate()) |
prompts in the conversation buffer; answer with :ai-accept / :ai-reject. ai-mode = auto-accept pre-answers ask only — it can never turn a deny into an allow |
mae-agent --prompt |
denies — no human attached, and it says so |
| External MCP dispatch (VS Code/Copilot, Claude Code via the shim) | denies — MAE implements no MCP elicitation, and the requesting client is not the local human |
mae --self-test |
denies — headless by definition |
If you drive MAE from an external editor's agent, raise auto_approve_tier deliberately for that
deployment; that path cannot prompt, so ask and deny look the same from the client's side.
Warning
Tiers bound; they do not prevent. A prompt is a usability mechanism that makes a restrictive
default affordable — it is not a security control. Anthropic reports users approve ~93% of
permission prompts. Symlink escapes, path-canonicalisation bypasses, and exfiltration through an
allowlisted binary all stay within a granted tier, and composition is unbounded: a write-tier
agent can edit a Makefile and then ask you to build. For genuinely untrusted input, run MAE in a
container. See ADR-084's "What this does not fix" for the full, deliberately unflattering list.
Setting the tier. Only two surfaces reach the enforced policy:
MAE_AI_PERMISSIONS=readonly|write|shell|privileged(environment variable), orauto_approve_tierinconfig.toml's[ai]section. Parsing is case-insensitive and accepts the documented aliases (standard,trusted,full,read-only); an unrecognised value is a startup error naming the bad value and the valid ones, rather than failing open.
The ai_tier editor option ((set-option! "ai_tier" …) / :set ai-tier …) currently changes only the
status-bar badge and does not alter the enforced policy (ADR-084 D7, still open). Earlier revisions
of this document referred to an option named permission_tier; no such option exists.
Workspace trust — A project-local .mae/init.scm is arbitrary Scheme, and Scheme can spawn processes, so evaluating one is equivalent to running the project's code. MAE evaluates it only from a directory listed in ~/.config/mae/trusted-projects (one absolute path per line; # comments allowed). Untrusted directories are skipped with a message naming the file and the line to add. Trust is exact-match: it is deliberately not inherited by subdirectories, so trusting a project does not trust a vendored dependency cloned inside it. A missing, unreadable, or malformed trust list trusts nothing.
Trust can only be granted by editing that file. There is no command, Scheme primitive, or MCP tool that grants it — an agent able to grant trust could then write .mae/init.scm and escalate across a restart. For the same reason, AI-originated writes to MAE's own configuration (~/.config/mae/** and any .mae/**) are refused across create_file, rename_file, and buffer saves; a human editing their own config, including :set-save, is unaffected.
The legacy v0.6 fallbacks that loaded a bare init.scm or scheme/init.scm from the working directory have been removed — those filenames are too ordinary to be safe. Move such a file to ~/.config/mae/init.scm.
Watchdog thread — A background thread monitors AI operations for stalls. If an AI operation exceeds 10 seconds without progress, the watchdog captures a backtrace and triggers auto-recovery. The user can also cancel via Esc or Ctrl-C (input lock).
Stagnation scoring — Semantic progress checkpoints are evaluated every 10 rounds. If the AI makes no meaningful progress (repeating the same actions), it receives escalating warnings and is eventually aborted.
Oscillation detection — Detects A-B-A-B action patterns (the AI undoing and redoing the same change) and issues a warning, then aborts if the pattern continues.
Budget guards — Per-session cost limits with configurable warn and hard-cap thresholds. Prevents runaway API spending.
Input lock — During AI operations, keyboard input is locked to prevent interference. Esc or Ctrl-C cancels the operation cleanly.
CI advisory enforcement — cargo-deny runs in CI to check for known security advisories in dependencies.
Shell blocklist — 6 hardcoded catastrophic patterns are blocked before shell execution:
rm -rf /,rm -fr /,mkfs.,dd if=,:(){ :,>(){ :- This is substring matching — a defense-in-depth measure, not a sandbox.
Context trimming — Token-aware context management prevents unbounded memory growth. However, there is no secret filtering — API keys or sensitive data in buffer content may be sent to the AI provider.
Babel code execution — Org-babel code blocks have configurable trust policies:
Never— never execute automaticallyNoExport— skip during exportYes— execute (requires explicit user confirmation)Query— prompt the user each time
No filesystem sandboxing — The AI agent can read and write any file the user's process can access. There is no seccomp, landlock, or container-based isolation. If you run MAE with untrusted AI prompts or org files, run it inside a container (see below).
MCP socket — The Unix socket at /tmp/mae-{PID}.sock is protected by filesystem permissions only. Any process running as the same user can connect. There is no per-client authentication or token-based auth.
Shell blocklist is bypassable — The blocklist uses simple substring matching. Commands chained with &&, ||, or ; after the blocked pattern, or commands using variable expansion, can bypass it. This is by design — the blocklist catches accidental catastrophic commands, not adversarial input.
Transcripts contain raw output — Conversation transcripts saved to ~/.local/share/mae/transcripts/ include raw tool call results. If a buffer contains secrets (API keys, passwords), those may appear in transcripts. Review transcripts before sharing.
Babel has no process isolation — Code block execution runs in a subprocess with the same permissions as MAE. There is no resource limiting (CPU, memory, network) beyond what the OS provides.
- Secrets are never plaintext in config.toml.
config.tomlis a legacy bootstrap; do not store API keys or the collab PSK in it directly. - API keys: Use
api_key_commandwith a password manager (e.g.,api_key_command = "pass show anthropic/api-key"), not plaintextapi_key. - Collab secrets: Never put
collab_pskplaintext in config.toml — usecollab_psk_command(shell to pass/keychain), or preferablycollab_auth_mode = "key"(Ed25519 trusted-peer mTLS) with the keystore at$XDG_DATA_HOME/mae/collab/trusted_keys. - Permission tier: The shipped default is now
readonly(reads auto-approved, writes and shell asked). Raise it deliberately viaMAE_AI_PERMISSIONS(e.g.MAE_AI_PERMISSIONS=write) orauto_approve_tierunder[ai]inconfig.toml— and remember that a non-interactive surface (external MCP,--prompt,--self-test) denies rather than asks, so those deployments need an explicit tier. Theai_tiereditor option does not affect enforcement — see the warning above. Do not rely on any tier as a boundary against an adversarial or prompt-injected model; run MAE in a container for genuinely untrusted input. - Untrusted files: Run MAE in a container when opening untrusted org files or working with untrusted AI prompts (see below).
- Transcripts: Review files in
~/.local/share/mae/transcripts/before sharing or committing them. - MCP access: The MCP socket is ephemeral (per-process PID). Only grant
mae-mcp-shimaccess to tools appropriate for your trust level.
MAE includes a Dockerfile for isolated execution:
# Quick: run mae in a container with a project directory mounted read-only
docker compose build runtime
docker run --rm -it -v /path/to/project:/work:ro mae mae /work/file.org
# Persistent config across runs
docker run --rm -it \
-v ~/.config/mae:/home/mae/.config/mae \
-v /path/to/project:/work \
mae mae /work/file.org
# Maximum isolation (no network)
docker run --rm -it --network=none \
-v /path/to/project:/work:ro \
mae mae /work/untrusted.orgThe container runs as a non-root mae user with pre-created XDG directories.
Terminal mode only — GUI requires display forwarding (X11/Wayland).