Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"Bash(git branch *)", "Bash(git checkout *)", "Bash(git status *)",
"Bash(git diff *)", "Bash(git log *)", "Bash(git show *)",
"Bash(git merge *)", "Bash(git stash *)",
"Bash(git restore *)", "Bash(git reset *)", "Bash(git rm *)",
"Bash(git mv *)", "Bash(git worktree *)",
Comment on lines +14 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

git reset can cause irreversible data loss when used with --hard.

Moving git reset * to allow means destructive commands like git reset --hard will execute without prompting. This can permanently discard uncommitted work.

Note the inconsistency: git clean is in the deny list (line 35) because it removes untracked files, yet git reset --hard (which discards uncommitted staged/unstaged changes) would now run without confirmation.

Consider one of these alternatives:

  1. Keep git reset in ask to require confirmation for potentially destructive resets
  2. Add Bash(git reset --hard *) to the deny list while allowing safer variants
  3. Accept the risk if the workflow prioritizes speed over safety for uncommitted changes

The other four commands (git restore, git rm, git mv, git worktree) are reasonable additions to allow as they are routine and less destructive.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/settings.json around lines 14 - 15, The entry "Bash(git reset *)" in
the allow list is risky because it permits destructive variants like "git reset
--hard"; update the settings to prevent accidental data loss by either moving
"Bash(git reset *)" back to ask, or keep a safe allow and add a deny entry
specifically for "Bash(git reset --hard *)" (or both), and ensure consistency
with the existing "git clean" deny; edit the settings to replace or augment the
"Bash(git reset *)" token with the chosen safer option so the policy enforces
confirmation or denial for hard resets.

"Bash(git remote *)", "Bash(git submodule *)", "Bash(git tag *)",
"Bash(git switch *)", "Bash(git rev-parse *)", "Bash(git cherry-pick *)",
"Bash(git blame *)", "Bash(git reflog *)", "Bash(git ls-files *)",
Expand Down Expand Up @@ -41,8 +43,7 @@
"Bash(gh workflow enable *)", "Bash(gh workflow disable *)",
"Bash(gh issue create *)", "Bash(gh issue comment *)",
"Bash(gh issue close *)", "Bash(gh issue edit *)",
"Bash(git reset *)", "Bash(git init *)", "Bash(git clone *)",
"Bash(git rm *)", "Bash(git mv *)", "Bash(git restore *)", "Bash(git worktree *)",
"Bash(git init *)", "Bash(git clone *)",
"Bash(uv remove *)", "Bash(uv cache *)", "Bash(uv init *)",
"WebFetch"
]
Expand Down
24 changes: 14 additions & 10 deletions tests/test_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,17 +328,21 @@ def test_pr_merge_requires_confirmation(self, settings: dict[str, Any]) -> None:
def test_workflow_run_requires_confirmation(self, settings: dict[str, Any]) -> None:
assert evaluate("Bash(gh workflow run deploy.yml)", settings) == "ask"

def test_git_reset_requires_confirmation(self, settings: dict[str, Any]) -> None:
assert evaluate("Bash(git reset --hard HEAD~1)", settings) == "ask"
assert evaluate("Bash(git reset HEAD file.py)", settings) == "ask"
def test_git_reset_is_allowed(self, settings: dict[str, Any]) -> None:
assert evaluate("Bash(git reset --hard HEAD~1)", settings) == "allow"
assert evaluate("Bash(git reset HEAD file.py)", settings) == "allow"

def test_git_destructive_operations_require_confirmation(self, settings: dict[str, Any]) -> None:
for cmd in ["git init", "git clone https://github.com/repo", "git rm file.py", "git mv a.py b.py"]:
def test_git_init_clone_require_confirmation(self, settings: dict[str, Any]) -> None:
for cmd in ["git init", "git clone https://github.com/repo"]:
assert evaluate(f"Bash({cmd})", settings) == "ask", f"{cmd} should require confirmation"

def test_git_restore_requires_confirmation(self, settings: dict[str, Any]) -> None:
assert evaluate("Bash(git restore file.py)", settings) == "ask"
assert evaluate("Bash(git restore --staged file.py)", settings) == "ask"
def test_git_rm_mv_are_allowed(self, settings: dict[str, Any]) -> None:
for cmd in ["git rm file.py", "git mv a.py b.py"]:
assert evaluate(f"Bash({cmd})", settings) == "allow", f"{cmd} should be allowed"

def test_git_restore_is_allowed(self, settings: dict[str, Any]) -> None:
assert evaluate("Bash(git restore file.py)", settings) == "allow"
assert evaluate("Bash(git restore --staged file.py)", settings) == "allow"

def test_gh_issue_mutations_require_confirmation(self, settings: dict[str, Any]) -> None:
for cmd in [
Expand All @@ -359,8 +363,8 @@ def test_gh_workflow_enable_disable_requires_confirmation(self, settings: dict[s
assert evaluate("Bash(gh workflow enable deploy.yml)", settings) == "ask"
assert evaluate("Bash(gh workflow disable deploy.yml)", settings) == "ask"

def test_git_worktree_requires_confirmation(self, settings: dict[str, Any]) -> None:
assert evaluate("Bash(git worktree add ../feature)", settings) == "ask"
def test_git_worktree_is_allowed(self, settings: dict[str, Any]) -> None:
assert evaluate("Bash(git worktree add ../feature)", settings) == "allow"

def test_uv_init_requires_confirmation(self, settings: dict[str, Any]) -> None:
assert evaluate("Bash(uv init my-project)", settings) == "ask"
Expand Down
Loading