Zora's security model has two tiers. The first tier — the PolicyEngine — defines what Zora is allowed to do. The second tier — the runtime safety layer — evaluates how risky a specific action is right now and stops to ask when the answer is "too risky."
The model is: Prevent → Pause → Prove.
| Phase | Mechanism | What It Does |
|---|---|---|
| Prevent | PolicyEngine + startup audit | Blocks structurally unsafe actions before they can be requested |
| Pause | Irreversibility scoring + approval queue | Pauses high-risk actions and waits for your explicit go-ahead |
| Prove | Audit log + session risk forecaster | Records everything; detects emerging risk patterns across a session |
All runtime safety features are disabled by default. Enable only what you need.
Add a [safety] section to ~/.zora/policy.toml:
[safety]
enabled = true
[safety.scoring]
enabled = true
[safety.approval]
enabled = true
channel = "telegram" # or "dashboard" — where approval requests appear
[safety.forecaster]
enabled = true
[safety.reputation]
enabled = true
[safety.audit]
startup_check = trueRestart the daemon after changing this file:
zora-agent stop && zora-agent startEvery tool call is scored 0–100 before it executes. The score represents how hard the action is to undo.
| Tool / Action | Score | Notes |
|---|---|---|
read_file |
5 | Read-only, no side effects |
web_fetch |
10 | Network read, no mutation |
write_file |
20 | File can be restored from git |
edit_file |
20 | Same as write |
bash (read-only: ls, cat, git diff) |
10 | Classified as safe |
bash (write: mkdir, cp, mv) |
35 | Reversible with effort |
bash (npm install, pip install) |
45 | State change, rollback needed |
git_commit |
55 | Commit exists, can be reverted |
git_push (origin) |
70 | Remote state changed |
send_signal_message |
80 | Message delivered, cannot unsend |
send_telegram_message |
80 | Same |
delete_file |
95 | No undo without backup |
bash (rm -rf) |
98 | Destructive, near-irreversible |
Scores are additive when actions chain. A git_push immediately after a git_commit within the same task scores as 70 (the push, the riskier of the two) plus 10 drift bonus if it wasn't in the original task scope.
# ~/.zora/policy.toml
[actions.thresholds]
warn = 40 # log a warning, allow the action
flag = 65 # pause and route to the approval queue
auto_deny = 95 # block outright, no approval optionActions scoring between warn and flag are logged with a [HIGH_RISK] tag but not blocked. Actions above flag are held until you respond. Actions at or above auto_deny are refused immediately — Zora explains why and asks if you want to lower the threshold for this session.
[actions.scores]
write_file = 15 # lower if you trust your git discipline
git_push = 50 # lower for trusted personal repos
delete_file = 80 # lower if you have reliable backupsWhen an action scores above flag, Zora pauses execution and routes the action to the approval queue.
Configure a Telegram bot token and your chat ID in ~/.zora/policy.toml:
[integrations.telegram]
bot_token = "env:ZORA_TELEGRAM_TOKEN" # never plaintext — use env: prefix
allowed_users = [123456789] # your Telegram numeric user ID
[safety.approval]
enabled = true
channel = "telegram"
timeout_seconds = 300 # auto-deny after 5 minutesWhen an action is flagged, you receive:
⚠️ Zora Action Approval Required
Action: git_push (origin main)
Risk: 70/100 (high)
Task: "update deployment scripts"
Token: ZORA-A8F2
Reply: /approve ZORA-A8F2 allow
/approve ZORA-A8F2 deny
/approve ZORA-A8F2 allow-30m
/approve ZORA-A8F2 allow-session
Approval scopes:
| Scope | What It Does |
|---|---|
allow |
Approve this one action |
deny |
Block this action; Zora explains and suggests alternatives |
allow-30m |
Approve all matching actions for the next 30 minutes |
allow-session |
Approve this action type for the rest of the session |
If you don't respond within timeout_seconds, the action is auto-denied.
Set channel = "dashboard" to handle approvals in the web UI at http://localhost:8070. Actions appear in the Pending Approvals panel with the same approval scope options.
The forecaster watches three risk signals across an entire session and escalates the approval threshold when the composite score passes a limit.
Drift — Has the agent's actions moved away from its original task?
Zora extracts keywords from the initial task description and compares them against the tools and file paths used as the session progresses. A session starting with "update deployment scripts" that starts touching billing code scores high on drift.
Salami — Is the agent executing a sequence of individually small actions that together build toward something harmful?
Each action is scored for contribution to known harmful patterns: privilege escalation, data exfiltration, lateral movement, credential gathering. No single action triggers this signal; it accumulates.
Commitment Creep — Are actions getting progressively more irreversible over time?
Tracks the moving average of irreversibility scores across the last 10 actions. If the average is trending up — starting with file reads, then writes, then git pushes, then message sends — the forecaster treats the next action as higher risk than its individual score suggests.
[safety.forecaster]
enabled = true
[safety.forecaster.thresholds]
drift_warn = 0.4 # 40% keyword divergence from original task
drift_flag = 0.7 # 70% divergence — next action needs approval
salami_warn = 30 # composite harm potential warning
salami_flag = 60 # composite harm potential — flag actions
creep_warn = 55 # moving avg irreversibility warning
creep_flag = 72 # moving avg irreversibility — flag actions
[safety.forecaster]
composite_flag = 1.5 # sum of normalized signals; flag when exceededWhen the composite score passes composite_flag, the effective flag threshold for new actions drops by 15 points — so actions that would normally pass silently get routed to approval.
When a spawned subagent repeatedly has actions blocked — either by policy or by the approval queue — it enters a progressive cooldown.
| Level | Trigger | Behavior |
|---|---|---|
| Normal | 0–2 blocked actions | No restrictions |
| Throttled | 3 blocked actions in a session | 2s delay before each action; warning logged |
| Restricted | 5 blocked actions | Every action needs explicit approval, regardless of score |
| Suspended | 8 blocked actions | Agent shut down; parent notified |
| Reset | 24 hours of clean behavior | Returns to Normal |
This prevents a misbehaving subagent from bypassing restrictions by simply retrying at volume.
[safety.reputation]
enabled = true
throttle_after = 3 # blocked actions before throttle
restrict_after = 5 # blocked actions before restrict
suspend_after = 8 # blocked actions before shutdown
reset_hours = 24 # hours of clean behavior to resetYou can restrict a subagent's permissions below the global ceiling without raising them. Drop a .zora/security-policy.toml in any project directory and Zora applies it when running tasks in that project.
This file can only restrict — it cannot grant permissions the global policy doesn't allow.
# .zora/security-policy.toml
[policy.tools]
# Tools in this list are denied even if the global policy allows them.
denied = ["bash", "spawn_zora_agent"]
# Tools in this list are the ONLY ones allowed (allowlist mode).
# If both denied and allowed are set, denied takes precedence.
allowed = ["read_file", "web_fetch", "write_file"]
[policy.filesystem]
# Additional paths to deny access to, on top of the global deny list.
deny = ["./secrets", "./credentials"]
# Restrict write access to specific subdirectories only.
write_allow = ["./output", "./reports"]
[policy.actions]
# Maximum irreversibility score for any action in this project.
# Overrides the global flag threshold — sets a hard ceiling, not just a warning.
max_irreversibility_score = 60 # nothing riskier than a git commit
# Override the flag threshold for this project only.
flag = 45
[policy.budget]
# Tighter action budget for this project, if you want it below the global default.
max_actions_per_session = 30
[policy.network]
# Restrict outbound network calls to specific domains.
allow_domains = ["api.github.com", "registry.npmjs.org"]
[policy.shell]
# Additional denied commands, on top of the global deny list.
deny = ["curl", "wget", "python3"]A code review agent has no reason to write files, run shell commands, or send messages:
# projects/my-app/.zora/security-policy.toml
[policy.tools]
allowed = ["read_file", "web_fetch"]
[policy.actions]
max_irreversibility_score = 10A reporting agent needs to write files but shouldn't touch git or send external messages:
[policy.tools]
denied = ["bash", "spawn_zora_agent", "send_signal_message", "send_telegram_message"]
[policy.filesystem]
write_allow = ["./reports"]
[policy.actions]
max_irreversibility_score = 25Every time the daemon starts, Zora runs a self-audit of its configuration files and environment.
zora security # full audit
zora security --fix # auto-fix WARN issues (does not touch FAIL issues)
zora security --json # machine-readable output$ zora security
✓ PASS ~/.zora/ permissions (700)
✓ PASS config.toml permissions (600)
✓ PASS policy.toml permissions (600)
✗ FAIL Bot token found in plaintext in config.toml:44
→ Move to env: ZORA_TELEGRAM_TOKEN and use env:ZORA_TELEGRAM_TOKEN
⚠ WARN Node.js 18.x — upgrade to 20 LTS for security patches
⚠ WARN ~/.zora/skills/ contains 1 unaudited skill: custom-reporter.skill
→ Run: zora-agent skill audit
Summary: 3 PASS, 1 FAIL, 2 WARN
FAILs block startup. Fix with: zora security --explain
| Check | Severity | Auto-Fix? |
|---|---|---|
~/.zora/ directory permissions (expect 700) |
FAIL | Yes |
config.toml permissions (expect 600) |
FAIL | Yes |
policy.toml permissions (expect 600) |
FAIL | Yes |
| Plaintext secrets in any config file | FAIL | No — requires manual remediation |
Symlinks in ~/.zora/ pointing outside |
FAIL | No |
| Node.js version below 20 LTS | WARN | No |
Unaudited skills in ~/.zora/skills/ |
WARN | No |
PolicyEngine [safety] section absent |
WARN | No (informational) |
auto_deny threshold at default 95 |
INFO | No |
FAILs block daemon startup entirely. WARNs are logged but do not block startup. INFO items are surfaced only when running zora security manually.
If you need to skip the audit for scripted deployments (not recommended):
[safety.audit]
startup_check = false- Run
zora-agent audit --last 20and look for[BLOCKED]entries. - Each entry includes the action name, the score it received, and which threshold triggered.
- To understand which rule is matching, run
zora-agent audit --last 1 --explain.
If the score seems wrong, override it for the specific tool in policy.toml:
[actions.scores]
git_push = 50 # adjust downward if you're comfortable with your repo- Verify
ZORA_TELEGRAM_TOKENis set in your environment. - Run
zora security— a misconfigured token shows as FAIL. - Check
~/.zora/logs/safety.logfor delivery errors. - Test the bot directly: send
/startto your bot in Telegram.
The session risk forecaster is elevating the effective threshold. Check ~/.zora/logs/forecaster.log for the current composite score breakdown.
To temporarily disable the forecaster for a session without changing your config:
zora-agent start --no-forecasterCheck the reputation log:
zora-agent audit --filter reputationIf a subagent was incorrectly suspended, reset its reputation manually:
zora-agent reputation reset <agent-id>Run zora security --explain for remediation steps specific to each failing check.
Common fixes:
# Fix directory permissions
chmod 700 ~/.zora
chmod 600 ~/.zora/config.toml ~/.zora/policy.toml
# Move plaintext token to environment variable
# In ~/.zora/config.toml, replace:
# bot_token = "123456:ABCdef..."
# With:
# bot_token = "env:ZORA_TELEGRAM_TOKEN"
# Then add to your shell profile:
export ZORA_TELEGRAM_TOKEN="123456:ABCdef..."- Security Guide — full OWASP coverage, PolicyEngine internals, trust levels
- Signal Channel Setup — configuring Signal as an inbound/outbound channel
- Troubleshooting — general troubleshooting reference