Skip to content

Commit 9aa740e

Browse files
committed
fix: clean permission model — feature branches are free, only merge asks
Hooks: on feature branches, commit/push are fully free (no prompts). Only merging into main prompts for approval. Bypass mode allows everything including merges. Updated both local hooks and all end-user templates (branch-context, merge-guard). Updated AGENTS.md git workflow section.
1 parent 24dd724 commit 9aa740e

3 files changed

Lines changed: 29 additions & 46 deletions

File tree

AGENTS.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,20 +93,26 @@ Every code change MUST include — **automatically, without the user asking:**
9393

9494
### Git workflow (mandatory, never skip)
9595

96-
1. **Commit often.** After each logical unit of work — a completed function,
97-
a passing test, a doc update. Don't batch unrelated changes.
96+
**The model:** Always work on a feature branch. On a feature branch,
97+
commit and push freely — no permissions needed. The only gate is
98+
merging back into main, which requires user approval (unless in
99+
bypass/autonomous mode).
98100

99-
2. **Meaningful commit messages.** Describe what changed and why, not "update
101+
1. **Create a feature branch first.** Before any code change:
102+
`git checkout -b feature/<task-slug>`. Never edit directly on main.
103+
104+
2. **On the feature branch: commit and push freely.** No prompts, no
105+
approval needed. Commit after each logical unit of work. Push after
106+
each commit. This is fully autonomous.
107+
108+
3. **Meaningful commit messages.** Describe what changed and why, not "update
100109
files" or "fix stuff". Format: imperative mood, under 72 chars for the
101110
first line. Example: `Add contact form with email validation`.
102111

103-
3. **No AI attribution in commits.** NEVER add `Co-Authored-By: Claude`,
112+
4. **No AI attribution in commits.** NEVER add `Co-Authored-By: Claude`,
104113
`Generated by AI`, `AI-assisted`, or any similar trailer or prefix.
105114
The commit is the user's work — the agent is a tool.
106115

107-
4. **Feature branches.** Never commit directly to main. Create a branch
108-
for each feature or fix (`feature/contact-form`, `fix/login-redirect`).
109-
110116
5. **Pull requests.** Create a PR for every feature branch. Use the PR
111117
template (`.github/pull_request_template.md`) which includes a test
112118
and documentation checklist.

packages/cli/templates/.claude/hooks/guard-branch-context.sh

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22
#
33
# guard-branch-context.sh — Claude Code PreToolUse hook
44
#
5-
# Fires before Edit and Write tool calls. If the current branch is `main`
6-
# or `master`, asks the user for confirmation before allowing file edits.
7-
#
8-
# Respects bypass/dangerous mode — user has opted into full autonomy.
5+
# Rules:
6+
# - On main/master → ask (agent should create a feature branch first)
7+
# - On any other branch → allow (feature branches are free to edit)
8+
# - Bypass mode → allow everything
99

10-
# Read the tool input from stdin
1110
INPUT=$(cat /dev/stdin)
1211

13-
# Respect bypass/dangerous mode
12+
# Bypass mode — full autonomy
1413
SETTINGS="$HOME/.claude/settings.json"
1514
if [ -f "$SETTINGS" ]; then
1615
BYPASS=$(jq -r '.skipDangerousModePermissionPrompt // false' "$SETTINGS" 2>/dev/null)
@@ -19,21 +18,15 @@ if [ -f "$SETTINGS" ]; then
1918
fi
2019
fi
2120

22-
# Check if we're in a git repo
2321
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
2422
exit 0
2523
fi
2624

27-
# Get current branch
2825
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "")
26+
[ -z "$BRANCH" ] && exit 0
2927

30-
if [ -z "$BRANCH" ]; then
31-
exit 0
32-
fi
33-
34-
# If on main or master, ask before allowing edits
3528
if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
36-
jq -n --arg reason "guard-branch-context: You are on the '$BRANCH' branch. Create a feature branch before editing (e.g., git checkout -b feature/<name>). Approve to continue on '$BRANCH'." '{
29+
jq -n --arg reason "You are on '$BRANCH'. Create a feature branch first (git checkout -b feature/<name>), or approve to edit on '$BRANCH'." '{
3730
hookSpecificOutput: {
3831
hookEventName: "PreToolUse",
3932
permissionDecision: "ask",
@@ -43,5 +36,4 @@ if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
4336
exit 0
4437
fi
4538

46-
# Not on main — allow
4739
exit 0

packages/cli/templates/.claude/hooks/guard-main-merge.sh

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,16 @@
22
#
33
# guard-main-merge.sh — Claude Code PreToolUse hook
44
#
5-
# Intercepts Bash tool calls and routes any command that looks like a merge
6-
# or push to main through interactive user approval. This prevents AI agents
7-
# from accidentally merging or pushing to main without explicit consent.
8-
#
9-
# Decision matrix:
10-
# `git merge ...` → ask (catches merge into any branch)
11-
# `git push ... main ...` → ask (catches push origin main, push HEAD:main, etc.)
12-
# anything else → allow
13-
#
14-
# Shipped by default with every webjs app via `webjs create`.
15-
# Wired in via .claude/settings.json under hooks.PreToolUse.
5+
# Rules:
6+
# - git merge → ask (merging to parent branch needs approval)
7+
# - git push on a feature branch → allow (free to push)
8+
# - git push targeting main → ask
9+
# - Bypass mode → allow everything
1610

17-
# Read the tool input from stdin (JSON shape: { tool_input: { command: ... }, ... })
1811
COMMAND=$(jq -r '.tool_input.command // empty' < /dev/stdin)
12+
[ -z "$COMMAND" ] && exit 0
1913

20-
# Empty command — let it pass.
21-
if [ -z "$COMMAND" ]; then
22-
exit 0
23-
fi
24-
25-
# Respect bypass/dangerous mode — user has opted into full autonomy.
14+
# Bypass mode — full autonomy
2615
SETTINGS="$HOME/.claude/settings.json"
2716
if [ -f "$SETTINGS" ]; then
2817
BYPASS=$(jq -r '.skipDangerousModePermissionPrompt // false' "$SETTINGS" 2>/dev/null)
@@ -31,7 +20,6 @@ if [ -f "$SETTINGS" ]; then
3120
fi
3221
fi
3322

34-
# Normalize whitespace so multi-line / heredoc commands match the same way.
3523
NORMALIZED=$(printf '%s' "$COMMAND" | tr -s '[:space:]' ' ')
3624

3725
ask_with_reason() {
@@ -45,15 +33,12 @@ ask_with_reason() {
4533
exit 0
4634
}
4735

48-
# Catch `git merge` anywhere in the command.
4936
if [[ "$NORMALIZED" == *"git merge"* ]]; then
50-
ask_with_reason "guard-main-merge: this command contains 'git merge'. Every merge requires your explicit approval. After merging, should the source branch be DELETED or KEPT? Approve to proceed (then tell the agent your preference)."
37+
ask_with_reason "This command contains 'git merge'. Merging requires approval. After merging, should the source branch be deleted or kept? Approve to proceed."
5138
fi
5239

53-
# Catch `git push` targeting main.
5440
if [[ "$NORMALIZED" == *"git push"* ]] && [[ "$NORMALIZED" == *"main"* ]]; then
55-
ask_with_reason "guard-main-merge: this command looks like 'git push' targeting main. Approve to proceed."
41+
ask_with_reason "This looks like 'git push' targeting main. Approve to proceed."
5642
fi
5743

58-
# Everything else passes through.
5944
exit 0

0 commit comments

Comments
 (0)