Skip to content

feat(cli): scope protected-file warnings to a project - #440

Open
NoRiceToday wants to merge 9 commits into
mainfrom
NoRiceToday/Reduce-protected-file-noise
Open

NoRiceToday wants to merge 9 commits into
mainfrom
NoRiceToday/Reduce-protected-file-noise

Conversation

@NoRiceToday

Copy link
Copy Markdown
Contributor

Issue: Closes #438

What

  • Batch mid-session protected-file detections into one decision dialog.
  • Add exact-path, batch, and project-scoped basename exceptions.
  • Keep launch-time protection and the audit trail unchanged.

Why

Creating a worktree can generate a burst of protected-pattern files such as .env and .env.local. omac cannot tell whether they are local defaults or real secrets, so it warns. The old watch produced one popup per file; an exact path does not cover the next worktree, and a global basename exception would silence that name in every project. A project-scoped exception is the narrowest scope that covers future worktrees of the same repository.

How

  • Resolve the project root by walking to the first .git entry; a validated linked worktree maps to the main repository. core.worktree is never read.
  • Store {root, name} in ~/.config/omac/prefs.yaml; match only below the canonical root.
  • Offer the project option last, after keep, exact paths, and the batch.
  • Refuse / and $HOME as roots. Non-git projects and worktrees outside the repository keep exact-path and batch choices.

Diagram

flowchart TD
  W[workdir] --> G{first .git entry}
  G -- directory --> R[parent = repo root]
  G -- file --> V{validated linked worktree?}
  V -- yes --> M[parent of common dir = main repo]
  V -- no --> L[directory holding .git = repo root]
  R --> C{contains workdir and<br/>not / or $HOME?}
  M --> C
  L --> C
  C -- yes --> P[project option offered]
  C -- no --> N[no project option]
Loading

Verification

  • go build ./... — pass
  • go vet ./internal/cli ./internal/prefs ./internal/netprompt ./internal/sandboxrun — pass
  • go test ./internal/prefs ./internal/netprompt ./internal/pathutil — pass
  • go test -race ./internal/cli -run 'Protected|GitProjectRoot|PathUnder|AskExceptions' — pass
  • go test ./internal/sandboxrun -run 'Worktree|Grant' -skip 'Integration' — pass
  • Full internal/cli still fails only on pre-existing sandbox limits (/tmp, ~/.config/omac).

Follow-up

Revoking a project exception still means editing prefs.yaml.

Sajjad Ahmad added 9 commits October 29, 2026 11:09
Detections from one scan arrive as one decision dialog instead of one
popup per file. The dialog offers, narrow to broad: ignore this exact
file, ignore all files in this batch, or ignore every file with that
basename in all projects. "Keep warning" is the default. Exceptions are
stored in ~/.config/omac/prefs.yaml; files stay protected at launch,
only the repeated notice is skipped.

The session-end report collapses a burst to one summary line, --verbose
lists every path, and the audit log keeps one event per path.

Closes #438

🤖 Generated with DeepSeek V4.1 Flash


Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
Captures the original noise requirements, the exact-path and batch
baseline from #438, the rejected global basename option, and a design
for a git-root-scoped name exception. No implementation yet; the tasks
list the follow-up work.

Refs #438

🤖 Generated with DeepSeek V4.1 Flash


Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
A worktree created under .worktrees/<name> brings new .env files with a new path, so an exact-path exception never covers the next worktree. Add one broader dialog choice: ignore a basename inside the repository.

The root comes from git common dir, which is shared by every worktree of the repo, and is accepted only when it contains the workdir. That keeps the scope off other projects and off a forged .git. Global basename exceptions stay out: .env.local often holds real secrets.

The file stays protected at launch and every detection is still audited. Exact-path and batch choices are unchanged.


Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
core.worktree lives in a config file the sandboxed agent can write, and
git rev-parse --show-toplevel honours it. A forged .git pointer could
therefore scope a project exception to a parent tree that holds other
checkouts.

Discover the root by walking up to the first .git entry instead. A .git
directory roots at its parent. A .git file roots at the main repository
only when sandboxrun.ResolveWorktreeCommonDir validates the worktree
back-pointer and layout; a submodule, separate git dir, or forged gitfile
stays at the directory that holds the file. The containment check plus
the root/home/shallow-ancestor gates reject every broader scope.

Lift the duplicated canonicalisation and containment helpers into
internal/pathutil.

Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
Three related defects in the mid-session watch:

- A broken prefs.yaml was silently replaced with an empty store, dropping
  every stored preference on the next save. Keep the load error, run the
  watch with an in-memory-only store, and record one notice.
- "Ignore all" rewrote prefs.yaml once per path. Add
  AddProtectedWatchIgnores, which appends the missing paths and saves once.
- Exact-path matching used filepath.Clean, so a symlinked spelling
  re-prompted. Canonicalise both sides through pathutil.

Also document that w.ignored is owned by the watch goroutine, use
slices.DeleteFunc so removePath/removePaths stop writing through the
caller's slice header, note the scan pause in askExceptions, and show
"(8 listed)" when the batch covers more files than the dialog lists.

Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
The protected-file dialog can offer fourteen long options. The generic
zenity/kdialog path passed no width or height, so the batch and project
choices could land below the fold.

Pass dialogDimensions() into both backends, add --no-markup to zenity,
and stop Pango-escaping the plain-text kdialog labels. An option that
contains &, <, or > now returns the same string the caller looks up.
Delete the unused Choose helper and its duplicated five-minute timeout;
ChooseContext already has a caller-owned context.

Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
The design and tasks still described the rev-parse invocation that
trusted core.worktree. Rewrite the resolution section around the
filesystem walk: a .git directory roots at its parent, a validated
linked worktree at the main repository, and anything else at the
directory holding the .git file. State that core.worktree is ignored and
that the filesystem root, $HOME, and too-shallow ancestors are refused.

Also correct the dialog wording: "Ignore all N" covers every file in the
batch (showing "(8 listed)"), --verbose lists the warnings the user
kept, and the 10-second scan pauses while a dialog is open.

Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
The depth gate refused a strict ancestor with fewer than two path components. That hit legitimate layouts such as /repo or /app: the main checkout got the project option, its worktree did not, even though both resolve to the same git root.

Discovery already anchors the root at the directory that owns .git, so the depth heuristic only added a false negative. Keep the two concrete refusals, / and $HOME.


Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
"Keep warning" read like "warn me again soon", but the watch reports each path once per session. The choice saves no exception and keeps the file in the audit log and the session-end report.

Label it "Keep warning for N files (report at exit)" and say in the body and docs what keeping it means.


Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
@NoRiceToday
NoRiceToday force-pushed the NoRiceToday/Reduce-protected-file-noise branch from df3b043 to 6f84638 Compare October 29, 2026 14:02

This branch has not been deployed

No deployments
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.

Mid-session protected-file watch floods the user with one dialog and one end-of-session line per new .env

1 participant