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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{
"name": "autodev",
"description": "Autonomous development workflow skills for coding agents",
"version": "6.1.3",
"version": "6.1.4",
"source": "./",
"author": {
"name": "Jon Langevin",
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "autodev",
"description": "Autonomous development workflow skills for coding agents: design, review, planning, execution, monitoring, and retrospectives",
"version": "6.1.3",
"version": "6.1.4",
"author": {
"name": "Jon Langevin",
"email": "jon@gocodealone.com"
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "autodev",
"displayName": "Autonomous Dev Kit",
"description": "Autonomous development workflow skills for coding agents",
"version": "6.1.3",
"version": "6.1.4",
"author": {
"name": "Jon Langevin",
"email": "jon@gocodealone.com"
Expand Down
7 changes: 7 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Autonomous Dev Kit Release Notes

## v6.1.4 — 2026-05-28

PreToolUse guard quote-strip extended to all destructive-command checks.

- `hooks/pre-tool-scope-guard`: the force-push, history-rewrite, locked-plan-push, and default-branch-push checks now operate on the quote-stripped form of the Bash `tool_input.command` (`cmd_no_quotes`, already computed for the SUPERPOWERS_ self-bypass check). Previously these four checks scanned the raw command, which produced false-positive blocks when a destructive command appeared as a documentation example inside a quoted heredoc body — e.g. `gh pr create --body "$(cat <<EOF ... git push --force origin main ... EOF)"` was matched as a real force push, blocking the PR creation. Encountered during v6.1.3 release: PR #47's body quoted `git push --force` and the hook blocked the very PR meant to ship the v6.1.3 fix.
- `tests/hook-contracts.sh`: added a regression test that asserts no block fires when force-push appears inside a quoted-string body.

## v6.1.3 — 2026-05-27

PreToolUse / SubagentStop block contract fix for Codex compatibility.
Expand Down
24 changes: 17 additions & 7 deletions hooks/pre-tool-scope-guard
Original file line number Diff line number Diff line change
Expand Up @@ -168,24 +168,34 @@ case "$tool_name" in

record_session_lock "$cmd"

# All destructive-command checks below operate on cmd_no_quotes (computed
# above) rather than the raw $cmd. The quoted/heredoc-stripped form
# avoids false-positive blocks when a destructive command appears as a
# documentation example inside a quoted string -- e.g. the body of
# `gh pr create --body "$(cat <<EOF ... git push --force origin main ... EOF)"`
# would otherwise be matched as a real force push. The trade-off is that
# an attacker who hides a destructive command inside quoted args (e.g.
# `eval "git push --force"`) would slip through; that's an acceptable
# edge case versus the much more common false-positive on PR bodies.

# ── 1. Force push (always blocked) ──────────────────────────────────────
# Catches: --force, --force-with-lease, -f flag
# These overwrite remote refs and lose commits for anyone who already pulled.
if printf '%s' "$cmd" | grep -q 'git push' && \
printf '%s' "$cmd" | grep -qE '(--force-with-lease|--force| -f( |$)| -f$)'; then
if printf '%s' "$cmd_no_quotes" | grep -q 'git push' && \
printf '%s' "$cmd_no_quotes" | grep -qE '(--force-with-lease|--force| -f( |$)| -f$)'; then
block "Force push blocked — rewrites remote history and permanently discards commits that others may have pulled. During autonomous pipeline execution this is never acceptable. If the remote branch genuinely needs correction, stop and get explicit user approval. Unlock path: ask the user, then proceed manually with SUPERPOWERS_HOOKS_DISABLE=1 scoped to that one command."
fi

# ── 2. Local history rewrites (always blocked) ──────────────────────────
# git rebase -i: interactive rebase rewrites commit SHAs.
# git reset --hard: destructively discards work-tree and commit history.
if printf '%s' "$cmd" | grep -qE 'git (rebase[[:space:]]+-i|rebase[[:space:]].*-i|reset[[:space:]]+--hard)'; then
if printf '%s' "$cmd_no_quotes" | grep -qE 'git (rebase[[:space:]]+-i|rebase[[:space:]].*-i|reset[[:space:]]+--hard)'; then
block "Destructive history rewrite blocked (git rebase -i or git reset --hard). These commands alter commit history and can discard work irreversibly. Stop and ask the user whether this is genuinely intended before proceeding."
fi

# ── 3. push / PR creation while locked plan hash mismatches ─────────────
# Catches post-lock plan tampering before anything reaches the remote.
if printf '%s' "$cmd" | grep -qE '(git push|gh pr create|gh pr merge)'; then
if printf '%s' "$cmd_no_quotes" | grep -qE '(git push|gh pr create|gh pr merge)'; then
while IFS= read -r locked_plan; do
[ -n "$locked_plan" ] || continue
if ! verify_lock "$locked_plan"; then
Expand All @@ -195,14 +205,14 @@ case "$tool_name" in
fi

# ── 4. Push/commit to default branch (blocked unless opt-out) ───────────
if printf '%s' "$cmd" | grep -qE 'git (push|commit)' && \
if printf '%s' "$cmd_no_quotes" | grep -qE 'git (push|commit)' && \
[ "${SUPERPOWERS_ALLOW_DEFAULT_BRANCH:-}" != "1" ]; then
# Explicit main/master push target
if printf '%s' "$cmd" | grep -qE 'git push.+(origin[[:space:]]+(main|master)|HEAD:(main|master)|(main|master):[[:space:]]*(main|master))'; then
if printf '%s' "$cmd_no_quotes" | grep -qE 'git push.+(origin[[:space:]]+(main|master)|HEAD:(main|master)|(main|master):[[:space:]]*(main|master))'; then
block "Pushing directly to main or master is blocked during autonomous pipeline execution. Work on a feature branch and open a PR for review. If this is genuinely intentional, set SUPERPOWERS_ALLOW_DEFAULT_BRANCH=1 in your environment and retry the command manually."
fi
# Bare 'git push' or 'git push origin' when current branch is main/master
if printf '%s' "$cmd" | grep -qE 'git push[[:space:]]*(origin[[:space:]]*)?$'; then
if printf '%s' "$cmd_no_quotes" | grep -qE 'git push[[:space:]]*(origin[[:space:]]*)?$'; then
current_branch=$(cd "$cwd_dir" && git rev-parse --abbrev-ref HEAD 2>/dev/null || true)
if [ "$current_branch" = "main" ] || [ "$current_branch" = "master" ]; then
block "Current branch is '${current_branch}' (the default branch). Pushing directly to main or master is blocked during autonomous pipeline execution. Switch to a feature branch first. To override, set SUPERPOWERS_ALLOW_DEFAULT_BRANCH=1."
Expand Down
28 changes: 28 additions & 0 deletions tests/hook-contracts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,33 @@ test_session_start_json() {
assert_hook_context_json "session-start" "SessionStart" "$output"
}

test_pre_tool_scope_guard_does_not_block_force_push_inside_quoted_string() {
# Destructive-command regexes must use the quote-stripped form of the
# tool_input.command so that a documentation example inside a quoted
# heredoc body doesn't trigger a false-positive force-push block.
# Regression for the session-time block of PR #47 creation: the PR body
# quoted `git push --force origin main` verbatim and the hook matched it.
local tmp stdout_file stderr_file status
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' RETURN
stdout_file="$tmp/out"
stderr_file="$tmp/err"
payload='{"tool_name":"Bash","tool_input":{"command":"gh pr create --title hi --body \"Example to avoid: git push --force origin main\""},"cwd":"'"$tmp"'"}'
set +e
printf '%s' "$payload" | hooks/pre-tool-scope-guard >"$stdout_file" 2>"$stderr_file"
status=$?
set -e
if [ "$status" != "0" ]; then
fail "pre-tool-scope-guard: must not block force-push mention inside quoted string, exit ${status} stdout: $(cat "$stdout_file") stderr: $(cat "$stderr_file")"
return
fi
if grep -q '"decision":"block"' "$stdout_file"; then
fail "pre-tool-scope-guard: blocked force-push mention inside quoted string (false positive). stdout: $(cat "$stdout_file")"
return
fi
pass "pre-tool-scope-guard: does not block force-push mentions inside quoted strings"
}

test_pre_tool_scope_guard_block_exits_zero_with_stderr_reason() {
# When pre-tool-scope-guard blocks a Bash command, it must:
# (1) exit 0 -- both Claude Code and Codex ignore stdout JSON on exit 2
Expand Down Expand Up @@ -1477,6 +1504,7 @@ test_pretool_allows_locked_plan_text_edit
test_subagent_allows_non_manifest_plan_backport
test_subagent_scope_guard_ignores_unattributed_workspace_lock
test_subagent_scope_guard_blocks_attributed_drift
test_pre_tool_scope_guard_does_not_block_force_push_inside_quoted_string
test_pre_tool_scope_guard_block_exits_zero_with_stderr_reason
test_subagent_scope_guard_block_exits_zero_with_stderr_reason
test_scope_lock_claim_writes_session_attribution
Expand Down
Loading