Skip to content

Permissions

reyn's permission system gates access to file paths, shell, MCP tools, named tools, and Python preprocessor steps. Defaults are conservative; anything outside the defaults requires either a workflow-level declaration plus user approval, OR a project-wide pre-approval in reyn.yaml.

Default grants (no declaration needed)

Op Scope
file.read / file.glob / file.grep Any path under the project root (CWD).
file.write / file.edit / file.delete Only under <CWD>/.reyn/ or <CWD>/reyn/.

Anything outside these defaults must be declared.

Workflow declarations (permissions: in skill.md frontmatter)

Phase-level permissions: was removed. All permission declarations belong in skill.md frontmatter — see skill-md.md. Phases inherit whatever the workflow declares.

---
type: skill
name: example
entry: main
final_output: result
permissions:
  shell: true
  mcp: [my_server]
  tool: [web_search]
  file:
    read:
      - path: ~/notes
        scope: recursive
    write:
      - path: /tmp/output
        scope: just_path
  http.get:
    - host: api.github.com           # specific host: startup_guard prompts once, runtime silent
    - host: "*"                      # wildcard: runtime per-host 4-layer prompt for any URL
  secret.write:
    - GITHUB_TOKEN                   # specific key, or
    - "*"                            # wildcard for runtime-determined keys (= user-prompt is the gate)
  python:
    - module: stats
      function: compute
      mode: safe
      timeout: 30
---

shell

true to enable the shell Control IR op for this phase. Off by default.

mcp, tool

List of MCP server names / named tool ids the phase may call.

file.read / file.write

For paths outside the default zones. Each entry has:

  • path — absolute, or relative to CWD. ~ is expanded.
  • scopejust_path (this exact path) or recursive (this path and everything below it).

file.write covers write, edit, and delete ops.

python

Per-(module, function) declarations for python preprocessor steps. See reference/dsl/preprocessor.md.

  • module, function — must match the corresponding preprocessor step.
  • modesafe (sandboxed) or unsafe (no AST sandbox; needs --allow-unsafe-python at runtime).
  • timeout — wall-clock seconds before the parent SIGKILLs the child. Default 30.

http.get

Per-host HTTP allowlist for reyn.api.safe.http.* (workflow-internal) AND for web_fetch (LLM-driven) — both surfaces share one axis.

  • Specific host (http.get: [{host: "api.github.com"}]) — startup_guard prompts once per <skill, host>; runtime is silent after approval. Same model as file.write outside the default zone.
  • Wildcard (http.get: [{host: "*"}] or ["*"]) — host set is unknown at write-time (= LLM picks at runtime); the 4-layer prompt fires inside require_http_get at the actual host gate; ALWAYS / NEVER persists per host.
  • No declaration — legacy web.fetch compat fallback with DeprecationWarning until the migration window closes.

reyn.api.safe.http (subprocess path) accepts only specific hosts; wildcard requires the async web_fetch op route.

secret.write

Per-key allowlist for ~/.reyn/secrets.env writes (= called by the mcp_install op handler when persisting isSecret env vars).

  • Specific key (secret.write: ["GITHUB_TOKEN"]) — authorises that exact env-var name.
  • Wildcard (secret.write: ["*"]) — runtime-determined key set (= mcp_install reads isSecret env vars from the registry response). The operator's per-value prompt at op-execution time is the actual security gate.

Web ops

web_search is Tier 1: passes through by default without any declaration. Restrict project-wide via permissions.web.search: deny.

web_fetch is unified under the http.get axis (same per-host gate as safe.http). The chat router injects http.get: [{host: "*"}] so LLM-driven fetches keep working with per-host prompts replacing the old per-URL prompts. Legacy permissions.web.fetch: allow / deny config keys remain honored as backward-compat aliases during the migration window.

permissions:
  web.search: deny   # block all web_search ops
  web.fetch: deny    # legacy alias — overrides http.get wildcard, raises immediately
  web.fetch: allow   # legacy alias — pre-approves any host (= equivalent to ALWAYS for all hosts)

This differs from Tier 2-3 ops (shell, mcp) which require an explicit declaration in skill.md before the op is even attempted.

Approval flow (interactive)

When a phase declares a non-default permission, reyn shows a single startup prompt:

[approval] my_skill/file.write needs:
  /tmp/output (just_path)

  [y] allow this run only
  [j] persist approval for this exact path + skill
  [r] persist approval for the parent dir (recursive) + skill
  [N] deny

Persistent choices land in .reyn/approvals.yaml keyed by <skill>/<op>/<path> (with a trailing / for recursive grants). External skills cannot reuse another skill's approvals — keys are skill-scoped to prevent privilege escalation.

Project-wide pre-approval (reyn.yaml)

permissions:
  shell: allow
  file.write: allow         # grants ALL write-class ops for ALL skills
  python:
    safe: allow             # auto-approve all safe-mode python steps
    unsafe: allow           # also requires --allow-unsafe-python at runtime
    allowed_modules:
      - math
      - statistics
      - mypackage

Use allow only when the project is trusted. ask (the default) prompts; deny rejects.

Non-interactive runs (CI, eval)

reyn eval runs non-interactively — there is no prompt. Approvals must be pre-arranged either in reyn.yaml or .reyn/approvals.yaml (e.g. by running the target workflow once interactively first).

Inspecting and revoking

reyn permissions list             # show saved approvals
reyn permissions revoke <key>     # remove an approval

See also