Skip to content
Closed
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
14 changes: 7 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ and the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

- **completion-integrity plugin (2026-01-07):**
- Prevents Claude from taking shortcuts to finish tasks
- **Pre-Commit Gate (PreToolUse: Bash):**
- Blocks `git commit` when staged changes contain integrity violations
- Detects: C#/JS/Python warning suppressions, commented-out tests, test file deletion
- Detects: Deleted assertions, empty catch blocks, excessive fresh TODOs
- **Phase-End Check (Stop):**
- Warns when Claude claims "done!" but response indicates shortcuts
- Detects: Untested claims, dismissed warnings, deferred work, deleted code
- **Git Pre-Commit Hook (Recommended):**
- `scripts/install-git-hook.sh` installs native git hook
- Works with ALL modes including `--dangerously-skip-permissions`
- Blocks commits with warning suppressions, commented tests, deleted assertions
- **Claude Plugin Hooks (when not in bypass mode):**
- PreToolUse: Intercepts `git commit` commands (bypassed with skip-permissions)
- Stop: Warns on premature completion claims (works in all modes)
- **Manual Script:**
- `scripts/integrity-check.sh` for on-demand scanning of staged changes

Expand Down
31 changes: 28 additions & 3 deletions plugins/completion-integrity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,41 @@ Claude sometimes:

## Solution

This plugin hooks into:
1. **Pre-commit** - Blocks commits with integrity violations
2. **Response end** - Warns when completion claims seem premature
Two enforcement mechanisms:

### 1. Git Pre-Commit Hook (Recommended)

Works with ALL modes, including `--dangerously-skip-permissions`:

```bash
bash scripts/install-git-hook.sh
```

This installs a native git hook at `.git/hooks/pre-commit` that blocks commits with violations.

### 2. Claude Plugin Hooks

Only work when Claude prompts for permission (NOT with bypass mode):
- **PreToolUse** - Intercepts `git commit` commands
- **Stop** - Warns when completion claims seem premature

## Install

```bash
/plugin install completion-integrity@ancplua-claude-plugins

# Then install the git hook (recommended):
bash "${CLAUDE_PLUGIN_ROOT}/scripts/install-git-hook.sh"
```

## Bypass Mode Compatibility

| Feature | Normal Mode | `--dangerously-skip-permissions` |
|---------|-------------|----------------------------------|
| Git pre-commit hook | ✅ Works | ✅ Works |
| PreToolUse hook | ✅ Works | ❌ Bypassed |
| Stop hook | ✅ Works | ✅ Works (informational) |
Comment on lines +44 to +48

Choose a reason for hiding this comment

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

low

The new compatibility table's columns are not aligned in the raw markdown source. To improve readability and adhere to the repository's style guide, please add padding to the table cells.

Suggested change
| Feature | Normal Mode | `--dangerously-skip-permissions` |
|---------|-------------|----------------------------------|
| Git pre-commit hook | ✅ Works | ✅ Works |
| PreToolUse hook | ✅ Works | ❌ Bypassed |
| Stop hook | ✅ Works | ✅ Works (informational) |
| Feature | Normal Mode | `--dangerously-skip-permissions` |
|---------------------|-------------|----------------------------------|
| Git pre-commit hook | ✅ Works | ✅ Works |
| PreToolUse hook | ✅ Works | ❌ Bypassed |
| Stop hook | ✅ Works | ✅ Works (informational) |
References
  1. Repository style guide line 79 requires that markdown tables must be padded. (link)


## What Gets Blocked

| Pattern | Type |
Expand Down
141 changes: 141 additions & 0 deletions plugins/completion-integrity/scripts/install-git-hook.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#!/usr/bin/env bash
# =============================================================================
# INSTALL GIT PRE-COMMIT HOOK
# =============================================================================
# Installs the completion-integrity check as a real git pre-commit hook.
# This works independently of Claude's permission system.
# =============================================================================

set -euo pipefail

# Colors for install script output
GREEN='\033[0;32m'
NC='\033[0m'

# Find git root
GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
if [[ -z "${GIT_ROOT}" ]]; then
echo "ERROR: Not in a git repository"
exit 1
fi

HOOKS_DIR="${GIT_ROOT}/.git/hooks"
PRE_COMMIT_HOOK="${HOOKS_DIR}/pre-commit"

Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The script assumes the .git/hooks directory exists, but in some edge cases (e.g., a freshly initialized git repo), this directory might not exist yet. Consider adding a check to create it if needed: 'mkdir -p "${HOOKS_DIR}"' before line 26.

Suggested change
mkdir -p "${HOOKS_DIR}"

Copilot uses AI. Check for mistakes.
# Check if pre-commit hook already exists
if [[ -f "${PRE_COMMIT_HOOK}" ]]; then
echo "WARNING: pre-commit hook already exists at ${PRE_COMMIT_HOOK}"
echo "Backing up to ${PRE_COMMIT_HOOK}.backup"
cp "${PRE_COMMIT_HOOK}" "${PRE_COMMIT_HOOK}.backup"
Comment on lines +28 to +29

Choose a reason for hiding this comment

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

low

The current backup mechanism for an existing pre-commit hook overwrites pre-commit.backup on each run. This means if you run the installer multiple times, you will lose older backups. To prevent data loss, consider using a timestamp in the backup filename to make it unique.

Suggested change
echo "Backing up to ${PRE_COMMIT_HOOK}.backup"
cp "${PRE_COMMIT_HOOK}" "${PRE_COMMIT_HOOK}.backup"
BACKUP_FILE="${PRE_COMMIT_HOOK}.backup.$(date +%s)"
echo "Backing up to ${BACKUP_FILE}"
cp "${PRE_COMMIT_HOOK}" "${BACKUP_FILE}"

fi

# Create the pre-commit hook
cat > "${PRE_COMMIT_HOOK}" << 'HOOK_EOF'
#!/usr/bin/env bash
# =============================================================================
# GIT PRE-COMMIT HOOK - Completion Integrity Check
# =============================================================================
# Blocks commits with integrity violations:
# - Warning suppressions (#pragma warning disable, eslint-disable, noqa)
# - Commented-out tests
# - Deleted assertions (>2)
# - Deleted test files
# =============================================================================
set -euo pipefail
# Colors
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
NC='\033[0m'
VIOLATIONS=()
WARNINGS=()

Choose a reason for hiding this comment

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

low

The WARNINGS array is initialized but never used within the script. It seems to be a remnant from another script. Removing it will make the code cleaner and easier to understand.

# Get staged diff, excluding documentation and scripts
STAGED_DIFF=$(git diff --cached --unified=0 -- \
':(exclude)*.md' \
':(exclude)**/hooks/scripts/*.sh' \
':(exclude)**/scripts/*.sh' \
':(exclude)**/*.test.*' \
':(exclude)**/*.spec.*' \
':(exclude)**/test-fixtures/**' \
2>/dev/null || echo "")
Comment on lines +57 to +64

Choose a reason for hiding this comment

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

high

The git diff command is configured to exclude test files (e.g., **/*.test.*, **/*.spec.*) from the diff analysis. However, RULE 2 of this script is specifically designed to detect commented-out tests, which are typically found only in these files. By excluding them, the check for commented-out tests is rendered ineffective. To ensure the hook functions as intended, these exclusion patterns for test files should be removed.

Suggested change
STAGED_DIFF=$(git diff --cached --unified=0 -- \
':(exclude)*.md' \
':(exclude)**/hooks/scripts/*.sh' \
':(exclude)**/scripts/*.sh' \
':(exclude)**/*.test.*' \
':(exclude)**/*.spec.*' \
':(exclude)**/test-fixtures/**' \
2>/dev/null || echo "")
STAGED_DIFF=$(git diff --cached --unified=0 -- \
':(exclude)*.md' \
':(exclude)**/hooks/scripts/*.sh' \
':(exclude)**/scripts/*.sh' \
':(exclude)**/test-fixtures/**' \
2>/dev/null || echo "")

if [[ -z "${STAGED_DIFF}" ]]; then
exit 0
fi
ADDED_LINES=$(echo "${STAGED_DIFF}" | grep -E '^\+[^+]' | sed 's/^\+//' || true)
DELETED_LINES=$(echo "${STAGED_DIFF}" | grep -E '^-[^-]' | sed 's/^-//' || true)
# RULE 1: Warning suppressions
CS_SUPPRESS=$(echo "${ADDED_LINES}" | grep -iE '#pragma\s+warning\s+disable|SuppressMessage' || true)
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The C# suppression pattern is incomplete compared to the existing integrity-check.sh script. The pattern should include 'DisableWarning' to match line 52 of integrity-check.sh which uses: '#pragma\s+warning\s+disable|SuppressMessage|DisableWarning'

Copilot uses AI. Check for mistakes.
if [[ -n "${CS_SUPPRESS}" ]]; then
VIOLATIONS+=("C# warning suppression")
echo -e "${RED}BLOCKED: C# warning suppression detected${NC}"
echo "${CS_SUPPRESS}" | head -3 | while read -r line; do echo " + ${line}"; done
fi
JS_SUPPRESS=$(echo "${ADDED_LINES}" | grep -iE 'eslint-disable|@ts-ignore|@ts-nocheck' || true)
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The JS/TS suppression pattern is incomplete compared to integrity-check.sh. Missing patterns: '@ts-expect-error' and 'tslint:disable'. The complete pattern should be: 'eslint-disable|@ts-ignore|@ts-nocheck|@ts-expect-error|tslint:disable' (see integrity-check.sh line 62)

Suggested change
JS_SUPPRESS=$(echo "${ADDED_LINES}" | grep -iE 'eslint-disable|@ts-ignore|@ts-nocheck' || true)
JS_SUPPRESS=$(echo "${ADDED_LINES}" | grep -iE 'eslint-disable|@ts-ignore|@ts-nocheck|@ts-expect-error|tslint:disable' || true)

Copilot uses AI. Check for mistakes.
if [[ -n "${JS_SUPPRESS}" ]]; then
VIOLATIONS+=("JS/TS warning suppression")
echo -e "${RED}BLOCKED: JS/TS warning suppression detected${NC}"
echo "${JS_SUPPRESS}" | head -3 | while read -r line; do echo " + ${line}"; done
fi
PY_SUPPRESS=$(echo "${ADDED_LINES}" | grep -iE '#\s*noqa|#\s*type:\s*ignore' || true)
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The Python suppression pattern is missing 'pylint:disable'. The complete pattern should be: '#\snoqa|#\stype:\signore|#\spylint:\s*disable' (see integrity-check.sh line 72)

Suggested change
PY_SUPPRESS=$(echo "${ADDED_LINES}" | grep -iE '#\s*noqa|#\s*type:\s*ignore' || true)
PY_SUPPRESS=$(echo "${ADDED_LINES}" | grep -iE '#\s*noqa|#\s*type:\s*ignore|#\s*pylint:\s*disable' || true)

Copilot uses AI. Check for mistakes.
if [[ -n "${PY_SUPPRESS}" ]]; then
VIOLATIONS+=("Python warning suppression")
echo -e "${RED}BLOCKED: Python warning suppression detected${NC}"
echo "${PY_SUPPRESS}" | head -3 | while read -r line; do echo " + ${line}"; done
fi
# RULE 2: Commented tests
COMMENTED_TESTS=$(echo "${ADDED_LINES}" | grep -iE '//\s*\[(Test|Fact|Theory)\]|//\s*(it|test|describe)\s*\(' || true)
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The commented test pattern is missing 'TestMethod' which is used in some C# test frameworks. The complete pattern should be: '//\s*[(Test|Fact|Theory|TestMethod)]' (see integrity-check.sh line 86)

Suggested change
COMMENTED_TESTS=$(echo "${ADDED_LINES}" | grep -iE '//\s*\[(Test|Fact|Theory)\]|//\s*(it|test|describe)\s*\(' || true)
COMMENTED_TESTS=$(echo "${ADDED_LINES}" | grep -iE '//\s*\[(Test|Fact|Theory|TestMethod)\]|//\s*(it|test|describe)\s*\(' || true)

Copilot uses AI. Check for mistakes.
if [[ -n "${COMMENTED_TESTS}" ]]; then
VIOLATIONS+=("Commented-out tests")
echo -e "${RED}BLOCKED: Commented-out test detected${NC}"
echo "${COMMENTED_TESTS}" | head -3 | while read -r line; do echo " + ${line}"; done
fi
# RULE 3: Deleted assertions (>2)
DELETED_ASSERTS=$(echo "${DELETED_LINES}" | grep -iE 'Assert\.|Should\.|Expect\(' || true)
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The assertion detection only uses C#-style patterns (Assert.|Should.|Expect(). This misses JavaScript/TypeScript assertions which use different patterns like 'expect(|assert.|should.' (lowercase). The integrity-check.sh script has separate detection for C# (line 139) and JS (line 151) assertions. Consider either adding JS-specific assertion detection or updating this pattern to catch both.

Suggested change
DELETED_ASSERTS=$(echo "${DELETED_LINES}" | grep -iE 'Assert\.|Should\.|Expect\(' || true)
DELETED_ASSERTS=$(echo "${DELETED_LINES}" | grep -iE 'Assert\.|Should\.|Expect\(|assert\.|should\.|expect\(' || true)

Copilot uses AI. Check for mistakes.
ASSERT_COUNT=0
[[ -n "${DELETED_ASSERTS}" ]] && ASSERT_COUNT=$(echo "${DELETED_ASSERTS}" | wc -l | tr -d ' ')
if [[ "${ASSERT_COUNT}" -gt 2 ]]; then
VIOLATIONS+=("${ASSERT_COUNT} assertions deleted")
echo -e "${RED}BLOCKED: ${ASSERT_COUNT} assertions deleted${NC}"
fi
# RULE 4: Test files deleted
DELETED_FILES=$(git diff --cached --name-only --diff-filter=D 2>/dev/null || true)
DELETED_TESTS=$(echo "${DELETED_FILES}" | grep -iE '\.test\.|\.spec\.|_test\.|Tests\.cs' || true)
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The test file deletion pattern is incomplete compared to integrity-check.sh. Missing pattern: 'Test.cs' (single test files). The complete pattern should be: '.test.|.spec.|_test.|Tests.cs|Test.cs' (see integrity-check.sh line 167)

Suggested change
DELETED_TESTS=$(echo "${DELETED_FILES}" | grep -iE '\.test\.|\.spec\.|_test\.|Tests\.cs' || true)
DELETED_TESTS=$(echo "${DELETED_FILES}" | grep -iE '\.test\.|\.spec\.|_test\.|Tests\.cs|Test\.cs' || true)

Copilot uses AI. Check for mistakes.
if [[ -n "${DELETED_TESTS}" ]]; then
VIOLATIONS+=("Test file(s) deleted")
echo -e "${RED}BLOCKED: Test file deleted${NC}"
echo "${DELETED_TESTS}" | while read -r f; do echo " - ${f}"; done
fi
# Summary
if [[ ${#VIOLATIONS[@]} -gt 0 ]]; then
echo ""
echo -e "${RED}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${RED}COMMIT BLOCKED - ${#VIOLATIONS[@]} integrity violation(s)${NC}"
echo -e "${RED}═══════════════════════════════════════════════════════════════${NC}"
echo ""
echo "Fix these issues or use --no-verify to bypass (not recommended)"
exit 1
fi
exit 0
HOOK_EOF

chmod +x "${PRE_COMMIT_HOOK}"

echo -e "${GREEN}✓ Git pre-commit hook installed at ${PRE_COMMIT_HOOK}${NC}"
echo ""
echo "The hook will now block commits with integrity violations."
echo "To bypass (not recommended): git commit --no-verify"
echo "To uninstall: rm ${PRE_COMMIT_HOOK}"
Loading