Desktop GUI for promoting Claude Code permission rules between scopes β without hand-editing JSON.
For the original problem statement, design goals, and non-goals, see
CLAUDE.md. It's the project brief; the README is the "how to use and hack on it" doc.
Claude Code reads settings from JSON files at several scopes. Moving a permission rule from Project to User (or anywhere else) today means editing both JSON files by hand, which is tedious and drops formatting. ClaudeScope shows every rule across the four recognized scopes (User / User-Local / Project / Local) side-by-side and moves rules between them with a click.
- Four-column scope view β User / User-Local / Project / Local, laid out broadest-on-the-left, with a "Combined permissions" panel above them. That panel is a union across scopes; it is not a full precedence-aware evaluation of what Claude Code resolves at runtime, and the subtitle says so.
- Atomic writes with revalidation β serialize β re-parse the produced JSON β tempfile + rename, with one-shot
.bakbackup per file per session. See Write strategy for the fine print. - Diff preview modal β shows before/after for both sides of a move with a proper diff, Esc/Enter/Tab-trap keyboard handling, focus restored to the triggering button on close
- Rule search / filter β press
/anywhere to focus, case-insensitive substring,m/nmatch counts per group - Auto-reload β
notify-based file watcher picks up external edits (hand-edited JSON, another editor, etc.) and refreshes the UI without losing state - Heuristic shape lint β subtle β badge on rules that don't match the shapes this UI knows (
Bash(...),WebFetch(domain:...),mcp__server__tool, etc.). Best-effort only: Claude Code's full rule grammar isn't publicly documented, so flagged rules may still work β the popover names the specific heuristic that tripped and disclaims its scope.
- Writes funnel through a single
atomic_write_jsonchokepoint: serialize β re-parse the produced bytes β tempfile in the same directory βrenameover target. Bad bytes never reach disk. - The rename is filesystem-atomic. On Unix the parent directory is also
fsync'd so the rename is crash-durable; on Windows the parent-dir flush is skipped (see the doc-comment onatomic_write_jsonfor the rationale βstd::fscannot open a directory handle for flushing without a small raw-winapi wrapper, and NTFS journals rename metadata inMoveFileEx). - A
FileStamp(mtime + length) captured at load time is rechecked just before the rename. If the file changed on disk between load and save, the write is refused with a "reload and retry" error rather than overwriting the external edit. The check has a microsecond-scale TOCTOU window between the recheck andpersist; a writer that lands inside that window will still be overwritten. - On the first write of a given file per session, the original is copied to
<file>.bak. Existing.bakfiles from previous runs are preserved, not clobbered. This is a one-shot backup attempt, not a best-effort write-through: if backup creation is required and the copy fails, the error is surfaced and the write is not attempted. - A move writes the destination first, then removes from the source. If the source write fails, the destination write is rolled back so the rule ends up in exactly one scope rather than being lost or duplicated. If the rollback itself fails the user sees an explicit error.
- Tauri 2 β Rust backend + webview
- Vanilla TypeScript + Vite β front end (intentionally tiny, no framework)
serde_jsonwithpreserve_orderβ key order survives round-tripsnotify-debouncer-miniβ cross-platform file watching with built-in debouncing- Biome 2 β formatter + linter + import sorter for the front end
claude-scope/
βββ biome.json Biome config (formatter + linter)
βββ index.html Vite entry
βββ src/ Front-end (TypeScript)
β βββ main.ts State + Tauri command wiring + keybindings
β βββ ui.ts DOM rendering, diff modal, search UI
β βββ lint.ts Shape-level rule-string lint
β βββ types.ts Shared types with Rust
β βββ styles.css
βββ src-tauri/ Rust backend
β βββ Cargo.toml
β βββ tauri.conf.json
β βββ capabilities/
β βββ src/
β βββ main.rs Tauri entry
β βββ lib.rs Builder + managed state + command registration
β βββ scope.rs Scope discovery (walk-up, resolve paths)
β βββ io_atomic.rs Atomic read/write + .bak tracker
β βββ model.rs SettingsDoc, permission add/remove, render
β βββ commands.rs load_scopes / diff_move / apply_move
β βββ watcher.rs notify-based file watcher
βββ .github/workflows/ci.yml build matrix (Linux/Windows/macOS) + MSRV + lint
-
Rust β stable, 1.88+ (imposed by Tauri 2.10's transitive deps). Install via rustup:
- Windows:
winget install Rustlang.Rustup(then restart your terminal) - macOS / Linux:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
The Tauri CLI is bundled as an npm dev dependency β
cargo install tauri-cliis not needed. Usenpm run tauri dev(notcargo tauri dev). - Windows:
-
Node.js 20+ and npm
-
Linux build deps (only on Linux):
sudo apt-get install libgtk-3-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
-
pre-commit (optional, strongly recommended β CI runs the same hooks):
pip install --user pre-commit pre-commit install
npm install
npm run tauri devRunning ClaudeScope against your real ~/.claude/ while developing risks corrupting your actual Claude Code settings if a write path regresses. Two ways to dogfood without that risk (#66):
Override scope discovery so all "user" / "user-local" lookups root at a throwaway directory instead of your real $HOME / %USERPROFILE%. The current project root can be pinned with --project (otherwise the front-end picker / cwd walk-up behave normally).
For npm run tauri dev, use the env vars β the npm β tauri-cli β cargo chain mangles -- separators and tauri-cli forwards trailing args as cargo flags (so cargo run --home β¦ errors out). Env vars sidestep the whole chain. CLI flags work fine against a built binary.
# Dev build (Linux / macOS):
CLAUDE_SCOPE_HOME=/tmp/scratch CLAUDE_SCOPE_PROJECT=/tmp/scratch/project npm run tauri dev
# Dev build (Windows PowerShell):
$env:CLAUDE_SCOPE_HOME = "$env:TEMP\cs-scratch"
$env:CLAUDE_SCOPE_PROJECT = "$env:TEMP\cs-scratch\project"
npm run tauri dev
# Built binary β CLI flags:
claude-scope --home /tmp/scratch --project /tmp/scratch/projectWhen either override is active, a yellow Sandbox mode banner sits under the toolbar showing the active paths, so you can't forget you're in scratch mode. The picker still works β --home keeps redirecting user-scope writes regardless of which project you switch to mid-session.
One-shot helper that seeds a fresh scratch dir with realistic example settings (allow / deny / ask rules across all four scopes, plus env / hooks / theme blocks so the move-key flow has data) and prints the launch command.
python scripts/scratch-home.py # seeds a temp dir, prints launch line
python scripts/scratch-home.py /tmp/sb # seeds /tmp/sb explicitly, idempotent
python scripts/scratch-home.py /tmp/sb --force # wipe + reseed# Rust unit tests (scope discovery, atomic writes, move logic, watcher plan)
(cd src-tauri && cargo test)
# Front-end type check
npx tsc --noEmitFor UI changes, also run the manual smoke pass in docs/SMOKE_TEST.md β it's a ~5-minute checklist covering the move flow, watcher reload, scope visibility, and side-effect verification.
# Check everything (TS/JS/JSON/CSS via Biome; Rust via fmt + clippy).
# This is what the `lint` CI job runs.
pre-commit run --all-files
# Or just the TS side via npm scripts:
npm run lint # biome check (lint + format check)
npm run format # biome format --write (applies formatting)
# Rust side by hand:
(cd src-tauri && cargo fmt --all --check && cargo clippy --all-targets -- -D warnings)npm run tauri build| Key | Action |
|---|---|
/ |
Focus the rule search input |
Esc (in search) |
Clear the filter |
Esc (in diff modal) |
Cancel the move |
Enter (in diff modal) |
Activate the focused button (Apply is the default focus) |
Tab / Shift+Tab (in diff modal) |
Cycle focus within the modal |
Every push to dev and every PR against dev runs four jobs in parallel, producing six check runs total:
buildβ a matrix job acrossubuntu-24.04,windows-latest,macos-latest. Each runsnpm ci,tsc --noEmit,vite build,cargo test --lib --locked.msrv (1.88)β validates the declared MSRV viacargo check --lib --tests --lockedon Rust 1.88.0lintβ runspre-commit/action@v3(Biome, rustfmt, clippy, hygiene hooks)securityβrustsec/audit-checkagainst the RustSec advisory DB +npm audit --package-lock-only --audit-level=high(reads the lockfile directly; nonode_modulesinstall, no lifecycle scripts)
Dependabot opens weekly grouped PRs for Cargo, npm, and GitHub Actions minor/patch bumps. Major bumps land as separate PRs.
Please use GitHub Private Vulnerability Reporting β see SECURITY.md for details.
Pushing a tag of the form v*.*.* triggers .github/workflows/release.yml, which builds installers for all three platforms and uploads them to a GitHub Release as a draft. The maintainer reviews + publishes from the GitHub UI.
To cut a release:
- Bump the version in three places (they have to stay in sync β
tauri-actionreadstauri.conf.json, butcargoandnpmeach have their own copy):src-tauri/tauri.conf.jsonβversionsrc-tauri/Cargo.tomlβversionpackage.jsonβversion
- Commit and merge that bump into
dev(and whatever downstream branch actually ships). - Tag and push:
git tag v0.1.0 git push origin v0.1.0
- Watch Actions. When all three matrix legs finish, a draft release appears at Releases with:
- Windows:
.msi(WiX installer) and.exe(NSIS) - macOS:
.dmgand.app.tar.gz(universal binary β runs on Intel and Apple Silicon) - Linux:
.deb,.rpm, and.AppImage
- Windows:
- Review the artifacts, edit release notes if you want, click Publish release.
Releases are currently unsigned. That means:
- Windows: first launch shows a SmartScreen warning (click "More info" β "Run anyway").
- macOS: first launch shows a Gatekeeper warning (right-click the app β Open β Open).
- Linux: no warning.
Signing would require a Windows code-signing certificate and/or an Apple Developer ID + notarization. Planned, but out of scope until there's demand.
The release workflow is also wired up for workflow_dispatch β useful for rebuilding an existing tag or testing workflow changes before cutting a real release. From the Actions UI, click Run workflow on the Release workflow and pick a branch:
- Leave
tag_nameblank β the workflow builds off the dispatched branch's HEAD, generates anightly-<full-sha>tag (full 40-char commit SHA, so two nightlies on the same commit share a draft but different commits never collide), and creates a draft pre-release titledClaudeScope nightly-<full-sha>. Delete the draft when you're done. - Set
tag_nameto an existing tag (e.g.v0.1.0) β the workflow checks out that exact tag and rebuilds its draft release. Useful for re-uploading artifacts if a platform job was flaky.
MIT Β© Brian Minier