Skip to content

feat(cli): add --bypass-permissions; decouple bypass from --no-tmux mode#92

Merged
SamPlvs merged 5 commits into
mainfrom
claude/bypass-permissions-flag
May 28, 2026
Merged

feat(cli): add --bypass-permissions; decouple bypass from --no-tmux mode#92
SamPlvs merged 5 commits into
mainfrom
claude/bypass-permissions-flag

Conversation

@SamPlvs
Copy link
Copy Markdown
Owner

@SamPlvs SamPlvs commented May 28, 2026

Summary

Adds an explicit --bypass-permissions flag to zo build and zo continue for auto-approving Claude Code tool-call prompts. Resolves the long-standing UX tension between "I want to walk away from the terminal" (a permission-prompt concern) and "I want to skip human gate review" (a gate-mode concern) — these are now independent, opt-in toggles with one sane implicit coupling.

Truth table

Flags Gates Tool-call prompts
--gate-mode supervised (default) human approves human approves each
--gate-mode supervised --bypass-permissions human approves gates auto-approved (walk away)
--gate-mode auto auto on success, pause on must-pass fail human approves each
--gate-mode auto --bypass-permissions auto on success, pause on fail auto-approved
--gate-mode full-auto all auto auto-approved (implicit)
--gate-mode full-auto --bypass-permissions all auto auto-approved (redundant)

⚠️ Behavior change worth highlighting in review

Previously, --no-tmux runs unconditionally injected --dangerously-skip-permissions into the Claude CLI command (baked-in at wrapper.py:376). That coupled visibility-mode (tmux vs headless) to safety-mode (prompts vs no-prompts), which is wrong.

After this PR: bypass is purely a function of the resolver cli_bypass OR gate_mode == "full-auto". Same behavior in tmux and headless. If you previously ran zo continue --no-tmux --gate-mode supervised and expected zero prompts, you now need to add --bypass-permissions explicitly.

The design lesson is captured in PRIORS as PR-038: A CLI Flag Should Map to One Concern.

What's in the diff

  • src/zo/permissions_overlay.py (new, 140 LOC): apply_bypass_overlay(claude_dir) writes the defaultMode: "bypassPermissions" overlay onto .claude/settings.local.json with a safe sibling backup; returns a restore callable. cleanup_stale_overlay(claude_dir) handles crash-recovery via a sentinel-marker pattern.
  • src/zo/wrapper.py: launch_lead_session / _launch_tmux / _launch_headless all gain bypass_permissions: bool = False. Tmux path applies the overlay + registers atexit restore. Headless path makes the existing CLI flag conditional.
  • src/zo/cli.py: new _resolve_bypass_permissions(*, cli_bypass, gate_mode) resolver; new --bypass-permissions Click option on build and continue; _launch_and_monitor threads the flag through and calls cleanup_stale_overlay() at every invocation.

Tests

+17 tests (+714 LOC across 3 test files), all passing:

  • tests/unit/test_permissions_overlay.py (new, 12 cases): existing settings / no settings / malformed JSON / stale-cleanup with-original / stale-cleanup no-original / non-dict-permissions defensive / idempotent restore / cleanup no-op when no backup / cleanup no-op when directory missing.
  • tests/unit/test_wrapper.py (+3): headless conditional flag (with bypass / without bypass / default), tmux overlay-applied-when-bypass-true, tmux overlay-skipped-when-bypass-false.
  • tests/unit/test_cli.py (+1): resolver truth-table (6 rows from the table above).

```
$ uv run pytest -q
760 passed, 7 skipped in 6.60s

$ bash scripts/validate-docs.sh
10 passed 0 failed 1 warnings (11 checks)
```

(Previously 743 passed, 9 validate-docs passed. The validate-docs improvement is because the test-count badge warning resolved naturally as the suite grew above 743.)

Live overlay roundtrip verified

In addition to unit tests, manually ran the apply → restore → simulated-crash → cleanup_stale_overlay cycle on a real temp filesystem. Confirmed:

  • Apply overlay → file contains both original allow (preserved) AND defaultMode: "bypassPermissions" (added). Backup file present.
  • Normal restore → file byte-exact match to original. Backup cleaned.
  • Simulated crash (apply, no restore) → orphan backup left on disk.
  • Next zo invocation calls cleanup_stale_overlay → original restored, backup cleaned.

All four crash-recovery paths checked.

What's NOT been tested

Honesty section: the unit tests verify that ZO writes the correct settings overlay. They cannot verify that Claude Code itself honors permissions.defaultMode: "bypassPermissions" in TUI mode the way the docs suggest. That's behavior on Claude Code's side, outside ZO's control surface.

Recommended 60-second smoke test before relying on this overnight:

  1. Merge this PR.
  2. git pull && uv pip install -e . in your ZO repo.
  3. Run zo continue --repo /some/test-project --bypass-permissions against a small non-prod project.
  4. Watch the left tmux pane for ~60 seconds. If the lead runs anything (e.g., git status, ls) and no "Do you want to proceed?" prompt appears, the mechanism works as designed.
  5. If prompts still appear, ping me; fallback is a 2-minute fix (broaden permissions.allow to ["Bash(*)", "Read(*)", ...], which is guaranteed to work because it's the same mechanism the existing settings.json already uses).

Backwards-compat guarantee

Running ZO without the new flag is identical to current behavior:

  • Default tmux mode without --bypass-permissions: zero changes, zero file mutations, prompts fire as today.
  • The new cleanup_stale_overlay() call at startup is a no-op when no .zo-backup file is present (verified in test_cleanup_no_op_when_no_backup).
  • All 743 existing tests still pass alongside the 17 new ones.

The only behavior change is the --no-tmux semantic (called out above). If you don't use --no-tmux, this PR is transparent for the default-path workflow.

Crash-recovery design (the riskiest piece)

The tmux overlay mutates the user's settings.local.json for the duration of a run. Three layers of safety ensure the original is always restored:

  1. atexit handler — fires on normal Python exit and on uncaught exceptions.
  2. Sibling backup file (settings.local.json.zo-backup) — left on disk so a kill -9 or system crash doesn't lose the original. A __ZO_NO_ORIGINAL_FILE__ sentinel marks the "we created this from nothing" case so cleanup deletes rather than restores.
  3. cleanup_stale_overlay() — called at every _launch_and_monitor startup. Detects an orphan backup from a crashed previous run and restores before launching the new one.

Combined, every termination path short of filesystem corruption recovers cleanly.

Memory protocol files updated per CLAUDE.md auto-protocol

  • memory/zo-platform/STATE.md — session 031 hand-off entry prepended (references PR-038 prior).
  • memory/zo-platform/DECISION_LOG.md — full FEATURE + BEHAVIOR-CHANGE entry at 2026-05-28T18:00:00Z with rationale and alternatives considered.
  • memory/zo-platform/PRIORS.md — new PR-038 entry with three rules (one-concern-per-flag, safe-by-default, contract tests for coupling) and the secondary Python-testability lesson about lazy imports vs mock.patch.

Manual verification checklist (recommended before merging)

  • zo continue --repo prod-001 --gate-mode supervised in tmux → prompts fire as before, no overlay written
  • zo continue --repo prod-001 --gate-mode full-auto in tmux → no prompts, overlay applied, restored on exit
  • Same as above, but Ctrl-C mid-run → original settings.local.json restored
  • Same as above, but kill -9 mid-run → orphan backup left; next zo command restores it (look for "Restored .claude/settings.local.json from a previous interrupted run." message)
  • zo continue --repo prod-001 --no-tmux --gate-mode supervised → prompts fire (behavior change — was silently bypassed before)
  • zo continue --repo prod-001 --no-tmux --gate-mode full-auto → no prompts (unchanged)

🤖 Generated with Claude Code

Adds an explicit --bypass-permissions flag on zo build / zo continue
for auto-approving Claude Code tool-call prompts. Effective bypass
resolves as `cli_bypass OR gate_mode == "full-auto"` (the latter
implicit because no-human-on-gates plus must-click-every-tool is a
self-contradicting UX).

Works identically in tmux and headless modes:

- Headless: --dangerously-skip-permissions now ONLY appended when
  bypass=True (previously baked in unconditionally; this restores
  symmetry between modes).
- Tmux: new permissions_overlay module writes
  permissions.defaultMode: "bypassPermissions" into the project's
  .claude/settings.local.json on launch, backs up the original to
  a sibling .zo-backup file, restores via atexit on exit.
  cleanup_stale_overlay() runs at every _launch_and_monitor
  invocation to recover from a crashed previous run.

Truth table:

  --gate-mode supervised                    → bypass off
  --gate-mode supervised --bypass-perms*    → bypass on (walk away)
  --gate-mode auto                          → bypass off
  --gate-mode auto --bypass-perms*          → bypass on
  --gate-mode full-auto                     → bypass on (implicit)
  --gate-mode full-auto --bypass-perms*     → bypass on (redundant)

Behavior change: previously `zo build --no-tmux --gate-mode supervised`
silently bypassed permissions. After this PR, it prompts as expected.
Add --bypass-permissions to restore the prior behavior.

Tests: +17 (test_permissions_overlay.py covers existing/no/malformed
settings + stale-cleanup paths; test_wrapper.py +3 cases for headless
conditional flag + tmux overlay apply/skip; test_cli.py +1 case for
the resolver truth table). pytest: 760 passed, 7 skipped (was 743 +
7). validate-docs: 10/11 (0 failures).

Docs: docs/cli/build.mdx gains a "Permission prompts" section with
the truth table + behavior-change note; docs/cli/overview.mdx adds
the flag to the shared options table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 28, 2026

Deploying zero-operators with  Cloudflare Pages  Cloudflare Pages

Latest commit: fdcb6b9
Status: ✅  Deploy successful!
Preview URL: https://d3f2609e.zero-operators.pages.dev
Branch Preview URL: https://claude-bypass-permissions-fl.zero-operators.pages.dev

View logs

@mintlify
Copy link
Copy Markdown

mintlify Bot commented May 28, 2026

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
personal-6078e1c9 🟢 Ready View Preview May 28, 2026, 7:54 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

SamPlvs and others added 4 commits May 28, 2026 20:59
Records the design lesson behind the --bypass-permissions PR: a CLI
flag should map to one concern; coupling visibility-mode (--no-tmux)
to safety-mode (bypass) silently bypassed user expectations. Three
rules with Why/How-to-apply, the verified solution, and the secondary
Python testability footnote about lazy imports defeating mock.patch.

STATE.md session 031 entry now cross-references PR-038.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI surfaced 8 ruff errors in code added by the previous commit that I
missed running locally (I had only run pytest + validate-docs.sh):

- 6× SIM105: try/except FileNotFoundError: pass → contextlib.suppress(FileNotFoundError)
  (in permissions_overlay.py — 4 call sites in apply_bypass_overlay's
  restore closure and cleanup_stale_overlay)
- 1× UP037: removed quotes from the "object | None" type annotation
  (Python 3.10+ supports the union syntax natively)
- 1× E501: split the 105-char line for self._bypass_restore_fn into
  a comment + assignment to stay under the 100-char limit
- 1× TC003: moved Path import to TYPE_CHECKING block (only used in
  annotations; runtime uses the parameter object, not the class)
- 1× UP035: import Callable from collections.abc, not typing
  (modern Python ABC convention)

Local verification before push:
  uv run ruff check src/zo/  → All checks passed!
  uv run pytest -q           → 760 passed, 7 skipped
  bash scripts/validate-docs.sh → 10 passed, 0 failed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI doesn't gate on tests/ but my new test file had 5 ruff violations.
Fixed for cleanliness:

- I001: import block re-sorted (auto-fix)
- F401: removed unused `import pytest` (auto-fix, fixture access
  works through pytest's collection mechanism without the import)
- F841: removed unused `restore = ...` assignment in the first test
  (the test verifies overlay-active state, never calls restore)
- TC003: moved `from pathlib import Path` into a TYPE_CHECKING block
  since Path is only referenced in annotations; runtime uses the
  `tmp_path` fixture object directly

Pre-existing I001 in test_cli.py:1059 (unrelated to my changes,
inside someone else's test_banner_renders_low_token_badge) left
untouched — out of scope for this PR.

Verified locally:
  uv run ruff check src/              → All checks passed!
  uv run ruff check tests/unit/test_permissions_overlay.py tests/unit/test_wrapper.py
                                       → All checks passed!
  uv run --python 3.11 pytest -q      → 760 passed, 7 skipped
  uv run --python 3.12 pytest -q      → 760 passed, 7 skipped
  bash scripts/validate-docs.sh        → Documentation is consistent

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ll CI matrix

Captures the lesson from PR #92's two-corrective-push cycle: my local
pre-push protocol (pytest + validate-docs) was a subset of the actual
CI surface (ruff src/ + pytest on 3.11 AND 3.12 + validate-docs). Three
rules:

1. Read .github/workflows/*.yml at the start of every code-change task
   and execute every step locally before pushing. The YAML is the
   source of truth, not the developer's memory.
2. Run the full language/runtime matrix locally (uv run --python 3.11
   AND 3.12), not just the default Python.
3. Lint your own additions on every scope ruff is configured for, not
   just where CI gates — keeps you from quietly growing the project's
   test-lint debt.

Verified checklist now codified in the prior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@SamPlvs SamPlvs merged commit b101480 into main May 28, 2026
5 checks passed
@SamPlvs SamPlvs deleted the claude/bypass-permissions-flag branch May 28, 2026 20:33
SamPlvs added a commit that referenced this pull request May 28, 2026
The ticker spawned `claude -p --model haiku` every 60 seconds during a
zo build / zo continue session to summarise recent agent events into a
one-line headline. User feedback: nobody uses it. Cost: ~60 subprocess
spawns per hour at ~$0.0001-$0.0003 each, totalling ~$0.06-$0.18 per
overnight run, plus the latency/CPU cost of the spawn churn. Pure waste
if the output goes unread, and the lead pane already shows the live
task list and agent events in real time.

Kept:
- _generate_session_summary() — single Haiku call at session end for a
  2-3 bullet wrap-up. ~$0.0002 per run, genuinely useful at close.
- _headline_buffer + the .append() event-capture calls in _print_status
  (still feed the end-of-session summary).
- --no-headlines flag — preserved for backwards compatibility. Its
  meaning narrows to "skip the end-of-session summary too" (it can no
  longer disable the ticker because the ticker doesn't exist).

Removed:
- _maybe_print_headline() function (~30 lines)
- _last_headline_time + _headline_interval timer vars
- The _maybe_print_headline() call inside _print_status
- All doc language about "Haiku-summarised headlines every 60 seconds"
  in build.mdx Step 5 + Live monitoring Card + options table + low-token
  accordion, overview.mdx shared options table, quickstart.mdx
  "What you'll see" list + low-token Note, COMMANDS.md, low-token-mode.mdx
  preset tables + Batch API note, low-token-preset.mdx tables + flags.

Behaviour change: anyone running zo build today sees a console headline
every 60s; after this PR they don't. End-of-session summary unchanged.

Pre-push verification (per PR-039 protocol):
  ruff check src/                           All checks passed!
  uv run --python 3.11 pytest -q            743 passed, 7 skipped
  uv run --python 3.12 pytest -q            743 passed, 7 skipped
  bash scripts/validate-docs.sh             9 passed, 0 failed

Note: PR #92 (bypass-permissions, +17 tests) is still open, so test
count baseline is 743 here rather than 760. This branch is independent
of #92.

Memory protocol updated per CLAUDE.md auto-protocol: STATE.md session
032 entry, DECISION_LOG.md FEATURE-REMOVAL entry with rationale and
alternatives.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SamPlvs added a commit that referenced this pull request May 28, 2026
The ticker spawned `claude -p --model haiku` every 60 seconds during a
zo build / zo continue session to summarise recent agent events into a
one-line headline. User feedback: nobody uses it. Cost: ~60 subprocess
spawns per hour at ~$0.0001-$0.0003 each, totalling ~$0.06-$0.18 per
overnight run, plus the latency/CPU cost of the spawn churn. Pure waste
if the output goes unread, and the lead pane already shows the live
task list and agent events in real time.

Kept:
- _generate_session_summary() — single Haiku call at session end for a
  2-3 bullet wrap-up. ~$0.0002 per run, genuinely useful at close.
- _headline_buffer + the .append() event-capture calls in _print_status
  (still feed the end-of-session summary).
- --no-headlines flag — preserved for backwards compatibility. Its
  meaning narrows to "skip the end-of-session summary too" (it can no
  longer disable the ticker because the ticker doesn't exist).

Removed:
- _maybe_print_headline() function (~30 lines)
- _last_headline_time + _headline_interval timer vars
- The _maybe_print_headline() call inside _print_status
- All doc language about "Haiku-summarised headlines every 60 seconds"
  in build.mdx Step 5 + Live monitoring Card + options table + low-token
  accordion, overview.mdx shared options table, quickstart.mdx
  "What you'll see" list + low-token Note, COMMANDS.md, low-token-mode.mdx
  preset tables + Batch API note, low-token-preset.mdx tables + flags.

Behaviour change: anyone running zo build today sees a console headline
every 60s; after this PR they don't. End-of-session summary unchanged.

Pre-push verification (per PR-039 protocol):
  ruff check src/                           All checks passed!
  uv run --python 3.11 pytest -q            743 passed, 7 skipped
  uv run --python 3.12 pytest -q            743 passed, 7 skipped
  bash scripts/validate-docs.sh             9 passed, 0 failed

Note: PR #92 (bypass-permissions, +17 tests) is still open, so test
count baseline is 743 here rather than 760. This branch is independent
of #92.

Memory protocol updated per CLAUDE.md auto-protocol: STATE.md session
032 entry, DECISION_LOG.md FEATURE-REMOVAL entry with rationale and
alternatives.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SamPlvs added a commit that referenced this pull request May 29, 2026
PR #92 added --bypass-permissions to zo build/continue and documented it in docs/cli/build.mdx and overview.mdx, but missed docs/COMMANDS.md (the terminal-command reference that lists the sibling flags). Add it to both usage blocks plus a Permission-prompts description matching the build.mdx framing.

Memory cascade: STATE.md session-033 hand-off + DECISION_LOG entry. validate-docs 10/10 (1 pre-existing warning). No code or tests touched.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant