feat(cli): add --bypass-permissions; decouple bypass from --no-tmux mode#92
Merged
Conversation
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>
Deploying zero-operators with
|
| 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 |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an explicit
--bypass-permissionsflag tozo buildandzo continuefor 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
--gate-mode supervised(default)--gate-mode supervised --bypass-permissions--gate-mode auto--gate-mode auto --bypass-permissions--gate-mode full-auto--gate-mode full-auto --bypass-permissionsPreviously,
--no-tmuxruns unconditionally injected--dangerously-skip-permissionsinto the Claude CLI command (baked-in atwrapper.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 ranzo continue --no-tmux --gate-mode supervisedand expected zero prompts, you now need to add--bypass-permissionsexplicitly.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 thedefaultMode: "bypassPermissions"overlay onto.claude/settings.local.jsonwith 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_headlessall gainbypass_permissions: bool = False. Tmux path applies the overlay + registersatexitrestore. Headless path makes the existing CLI flag conditional.src/zo/cli.py: new_resolve_bypass_permissions(*, cli_bypass, gate_mode)resolver; new--bypass-permissionsClick option onbuildandcontinue;_launch_and_monitorthreads the flag through and callscleanup_stale_overlay()at every invocation.Tests
+17 tests (
+714 LOCacross 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:
allow(preserved) ANDdefaultMode: "bypassPermissions"(added). Backup file present.zoinvocation callscleanup_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:
git pull && uv pip install -e .in your ZO repo.zo continue --repo /some/test-project --bypass-permissionsagainst a small non-prod project.git status,ls) and no "Do you want to proceed?" prompt appears, the mechanism works as designed.permissions.allowto["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:
--bypass-permissions: zero changes, zero file mutations, prompts fire as today.cleanup_stale_overlay()call at startup is a no-op when no.zo-backupfile is present (verified intest_cleanup_no_op_when_no_backup).The only behavior change is the
--no-tmuxsemantic (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.jsonfor the duration of a run. Three layers of safety ensure the original is always restored:atexithandler — fires on normal Python exit and on uncaught exceptions.settings.local.json.zo-backup) — left on disk so akill -9or 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.cleanup_stale_overlay()— called at every_launch_and_monitorstartup. 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 supervisedin tmux → prompts fire as before, no overlay writtenzo continue --repo prod-001 --gate-mode full-autoin tmux → no prompts, overlay applied, restored on exitkill -9mid-run → orphan backup left; nextzocommand 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