Skip to content

fix(codex): support repository-scoped pre-push opt-out#2557

Open
vex1br wants to merge 6 commits into
affaan-m:mainfrom
vex1br:fix/codex-prepush-repo-optout
Open

fix(codex): support repository-scoped pre-push opt-out#2557
vex1br wants to merge 6 commits into
affaan-m:mainfrom
vex1br:fix/codex-prepush-repo-optout

Conversation

@vex1br

@vex1br vex1br commented Jul 24, 2026

Copy link
Copy Markdown

What Changed

  • Added a repository-scoped .ecc-prepush-disable marker. When it is a tracked, regular index entry at the real checkout root, ECC skips only its pre-push suite.
  • The hook resolves the real checkout root, so linked worktrees are supported.
  • ECC_PREPUSH_FULL=1 forces the full pre-push suite even when the marker is present.
  • Updated installer guidance to distinguish the pre-push-only opt-out from .ecc-hooks-disable.

Why This Change

The global pre-push suite can be disproportionate for repositories whose authoritative validation is protected remote CI. The existing .ecc-hooks-disable marker is too broad because it also disables pre-commit checks.

Security and Defaults

  • Without a marker, behavior is unchanged: the full pre-push suite runs.
  • An absent, untracked, symlinked, mode 120000, or conflicted marker runs the full suite.
  • Marker content is never interpreted.
  • Pre-commit remains active; the secret-detection regression test passes.
  • The opt-out emits a fixed, sanitized log message.

Testing Done

  • Manual testing completed
  • Automated tests pass locally (node tests/run-all.js) — the full Windows run used a noncanonical Node 24/npm 12/global profile; after dependencies materialized, 3099/3105 tests passed. The six remaining failures are in unchanged environment/harness surfaces (WSL bash discovery, npm 12 pack JSON, global HOME/USERPROFILE state, and Windows buffer handling). The focused changed surface passes 27/27. Remote upstream CI remains mandatory.
  • Edge cases considered and tested

TDD evidence: initial RED 22/2 -> GREEN 24/0; review RED 24/2 -> GREEN 26/0; hardening RED 25/2 -> final GREEN 27/0. bash -n and git diff --check pass. Code review, security review, and final Ultra audit found no P0-P3 findings.

Type of Change

  • fix: Bug fix
  • feat: New feature
  • refactor: Code refactoring
  • docs: Documentation
  • test: Tests
  • chore: Maintenance/tooling
  • ci: CI/CD changes

Security & Quality Checklist

  • No secrets or API keys committed (ghp_, sk-, AKIA, xoxb, xoxp patterns checked)
  • JSON files validate cleanly (no JSON files changed)
  • Shell scripts pass shellcheck (not run)
  • Pre-commit hooks pass locally (secret-detection coverage passes)
  • No sensitive data exposed in logs or output
  • Follows conventional commits format

If you changed dependencies or package.json (bin / files / deps)

Not applicable; dependencies and package.json are unchanged.

If you added a skill, command, agent, hook, or CLI tool

Not applicable; this changes an existing hook and does not add a distributable component.

Documentation

  • Updated relevant documentation (installer guidance)
  • Added comments for complex logic
  • README updated (not needed)

Tracking

Tracks vex1br/metaread#69.

Rollback

Revert this PR. No migration or persistent state is involved.

@vex1br
vex1br requested a review from affaan-m as a code owner July 24, 2026 00:30
@ecc-tools

ecc-tools Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 35494397-5a17-4e10-b1df-fca2631cac70

📥 Commits

Reviewing files that changed from the base of the PR and between 7b03a83 and 778fee9.

📒 Files selected for processing (3)
  • scripts/codex-git-hooks/pre-push
  • scripts/codex/install-global-git-hooks.sh
  • tests/scripts/codex-hooks.test.js
📜 Recent review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (15)
**/*.sh

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Before running shell commands, explain destructive or networked actions and prefer read-only inspection first

Files:

  • scripts/codex/install-global-git-hooks.sh
{scripts,bin}/**

⚙️ CodeRabbit configuration file

{scripts,bin}/**: Focus on command injection, unsafe subprocess usage, path traversal, SSRF, secret exposure, and missing tests for new CLI behavior.

Files:

  • scripts/codex/install-global-git-hooks.sh
  • scripts/codex-git-hooks/pre-push
**/*.{js,ts,jsx,tsx,py,java,cs,go,rb,php,scala,kt}

📄 CodeRabbit inference engine (.cursor/rules/common-coding-style.md)

**/*.{js,ts,jsx,tsx,py,java,cs,go,rb,php,scala,kt}: Always create new objects, never mutate existing ones. Use immutable patterns to prevent hidden side effects and enable safe concurrency
Organize code into many small files (200-400 lines typical, 800 lines max) organized by feature/domain rather than by type
Always handle errors explicitly at every level and never silently swallow errors
Always validate all user input before processing at system boundaries
Use schema-based validation where available
Fail fast with clear error messages when validation fails
Never trust external data (API responses, user input, file content)
Ensure code is readable and well-named
Keep functions small (less than 50 lines)
Keep files focused (less than 800 lines)
Avoid deep nesting (more than 4 levels)
Do not use hardcoded values; use constants or configuration instead

Files:

  • tests/scripts/codex-hooks.test.js
**/*.{js,ts,jsx,tsx,py,java,cs,rb,go,php,swift,kt,rs,c,cpp,h,hpp}

📄 CodeRabbit inference engine (.cursor/rules/common-security.md)

No hardcoded secrets (API keys, passwords, tokens) - validate before any commit

Files:

  • tests/scripts/codex-hooks.test.js
**/*.{js,ts,jsx,tsx,py,java,cs,rb,go,php}

📄 CodeRabbit inference engine (.cursor/rules/common-security.md)

**/*.{js,ts,jsx,tsx,py,java,cs,rb,go,php}: All user inputs must be validated
Enable CSRF protection on all state-changing endpoints
Verify authentication and authorization for all protected endpoints
Implement rate limiting on all endpoints to prevent abuse
Ensure error messages do not leak sensitive data in responses

Files:

  • tests/scripts/codex-hooks.test.js
**/*.{js,ts,jsx,tsx,py,java,cs,rb,go,php,sql}

📄 CodeRabbit inference engine (.cursor/rules/common-security.md)

Use parameterized queries to prevent SQL injection

Files:

  • tests/scripts/codex-hooks.test.js
**/*.{js,ts,jsx,tsx,html,php,java,cs,rb,go}

📄 CodeRabbit inference engine (.cursor/rules/common-security.md)

Implement XSS prevention by sanitizing HTML output

Files:

  • tests/scripts/codex-hooks.test.js
**/*.{js,ts,jsx,tsx,py,java,cs,rb,go,php,swift,kt,rs,c,cpp,h,hpp,properties,yml,yaml,json,env,config}

📄 CodeRabbit inference engine (.cursor/rules/common-security.md)

NEVER hardcode secrets in source code - ALWAYS use environment variables or a secret manager

Files:

  • tests/scripts/codex-hooks.test.js
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/typescript-coding-style.md)

**/*.{ts,tsx,js,jsx}: Use spread operator for immutable updates in TypeScript/JavaScript instead of direct mutation
Use async/await with try-catch for error handling in TypeScript/JavaScript
Use Zod for schema-based input validation in TypeScript/JavaScript
No console.log statements in production code; use proper logging libraries instead

**/*.{ts,tsx,js,jsx}: Auto-format JavaScript/TypeScript files using Prettier after edit
Warn about console.log statements in edited files
Check all modified files for console.log statements before session ends

**/*.{ts,tsx,js,jsx}: Use the ApiResponse interface pattern with generic type parameter: interface ApiResponse<T> { success: boolean; data?: T; error?: string; meta?: { total: number; page: number; limit: number; } }
Implement custom React hooks following the pattern: export a named function with use prefix, generic type parameters, and proper useEffect cleanup for side effects

**/*.{ts,tsx,js,jsx}: Never hardcode secrets; always use environment variables for sensitive credentials like API keys
Throw an error when required environment variables are not configured to fail fast and ensure security prerequisites are met

Use Playwright as the E2E testing framework for critical user flows in TypeScript/JavaScript

Files:

  • tests/scripts/codex-hooks.test.js
**/*.{test,spec}.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{test,spec}.{js,ts,jsx,tsx}: Write tests before implementation (test-driven development); target 80%+ coverage
Achieve minimum 80% test coverage across all three layers: Unit, Integration, and E2E
Use AAA structure (Arrange / Act / Assert) in tests with descriptive test names that explain behavior under test

Files:

  • tests/scripts/codex-hooks.test.js
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{js,ts,jsx,tsx}: Always create new objects and never mutate in place; return new copies instead
Keep files between 200–400 lines typical, with a maximum of 800 lines
Extract helpers when a file exceeds 200 lines
Handle errors explicitly at every level; never swallow errors silently
Validate all user input before processing; use schema-based validation where available
Never trust external data (API responses, file content, query params); always validate
All user inputs must be validated and sanitized
Error messages must be scrubbed of sensitive internals
Use readable, well-named identifiers in all code
Keep functions under 50 lines
Keep files under 800 lines
Avoid nesting deeper than 4 levels
Implement comprehensive error handling in all code
Do not hardcode values; use constants or environment configuration instead
Do not use in-place mutation; always return new objects or state

Files:

  • tests/scripts/codex-hooks.test.js
**/*.{js,ts,jsx,tsx,json,env*}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Do not hardcode secrets, API keys, passwords, or tokens

Files:

  • tests/scripts/codex-hooks.test.js
**/*.{js,ts}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{js,ts}: Use parameterized queries for all database writes (no string interpolation)
Auth/authz must be checked server-side for every sensitive path
Rate limiting must be applied to all public endpoints

Files:

  • tests/scripts/codex-hooks.test.js
**/*.{jsx,tsx,js,ts}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

HTML output must be sanitized where applicable

Files:

  • tests/scripts/codex-hooks.test.js
**/*.{js,ts,env*}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Required environment variables must be validated at startup

Files:

  • tests/scripts/codex-hooks.test.js
🧠 Learnings (1)
📚 Learning: 2026-07-17T04:35:58.631Z
Learnt from: Seekers2001
Repo: affaan-m/ECC PR: 2281
File: tests/scripts/council-multi-model.test.js:24-34
Timestamp: 2026-07-17T04:35:58.631Z
Learning: In standalone JavaScript test harness files under `tests/scripts/` (e.g., `*.test.js` harnesses), it’s acceptable—and intended—for tests to report results via `console.log` using `PASS`/`FAIL` style output. Do not require introducing a dedicated test-reporting/logging library solely because these harnesses use `console.log` for reporting. Failures should be signaled by exiting the process with a non-zero exit code.

Applied to files:

  • tests/scripts/codex-hooks.test.js
🪛 ast-grep (0.44.1)
tests/scripts/codex-hooks.test.js

[warning] 112-112: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFileSync(path.join(primaryDir, 'tracked.txt'), 'fixture\n')
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename)


[warning] 114-114: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFileSync(path.join(primaryDir, '.ecc-prepush-disable'), 'Disable only the ECC pre-push suite.\n')
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename)


[warning] 400-403: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFileSync(
path.join(fixture.linkedDir, '.ecc-prepush-disable'),
'An untracked marker must not disable checks.\n'
)
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename)


[warning] 433-433: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFileSync(markerPath, 'tracked.txt\n')
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename)


[warning] 516-516: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFileSync(path.join(fixture.linkedDir, 'synthetic-secret.txt'), ${syntheticToken}\n)
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename)


[warning] 967-967: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFileSync(path.join(codexDir, 'git-hooks', 'pre-push'), 'utf8')
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename)

🔇 Additional comments (5)
scripts/codex-git-hooks/pre-push (1)

11-34: LGTM!

scripts/codex/install-global-git-hooks.sh (1)

64-66: LGTM!

tests/scripts/codex-hooks.test.js (3)

14-15: LGTM!

Also applies to: 71-142


360-548: LGTM!


967-970: LGTM!


📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added a tracked .ecc-prepush-disable opt-out for disabling pre-push checks on a repository.
    • Added ECC_PREPUSH_FULL=1 for temporarily forcing the complete pre-push check.
  • Documentation

    • Improved installation guidance for disabling all hooks, disabling only pre-push checks, and applying one-time overrides.
  • Bug Fixes

    • Improved pre-push behavior across linked worktrees and ensured pre-commit secret detection remains enabled.

Walkthrough

The pre-push hook now resolves the repository root, supports tracked .ecc-prepush-disable markers and overrides, and runs from the root. Tests cover linked worktrees and marker states, while installer guidance documents the new controls.

Changes

Pre-push opt-out handling

Layer / File(s) Summary
Repository-root and marker handling
scripts/codex-git-hooks/pre-push
The hook resolves repository context, checks disable files, evaluates tracked opt-out markers, honors overrides, and changes to the repository root.
Opt-out instructions and synchronization assertions
scripts/codex/install-global-git-hooks.sh, tests/scripts/codex-hooks.test.js
Installer output and sync assertions document repository-wide disablement, pre-push-only disablement, and full-run overrides.
Linked-worktree test coverage
tests/scripts/codex-hooks.test.js
Hermetic helpers and regression tests cover tracked, untracked, symlink-mode, deletion-only, override, pre-commit, and default-run behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Git
  participant PrePushHook
  participant VerificationSuite
  Git->>PrePushHook: Resolve repository root and git directory
  PrePushHook->>Git: Read tracked .ecc-prepush-disable metadata
  PrePushHook->>VerificationSuite: Skip or run pre-push checks
Loading

Suggested reviewers: affaan-m, affaan-m

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: repository-scoped pre-push opt-out support.
Description check ✅ Passed The description matches the patch and explains the marker, override behavior, tests, and rationale.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a repository-scoped .ecc-prepush-disable marker that lets individual repositories opt out of ECC's pre-push suite while leaving pre-commit checks (including secret detection) fully active. The hook resolves the true checkout root via git rev-parse --show-toplevel so linked worktrees are supported, and an ECC_PREPUSH_FULL=1 env var lets developers force the full suite even when the marker is present.

  • pre-push hook: Replaces the old --is-inside-work-tree guard with --show-toplevel to capture repo_root, adds index-based opt-out logic that validates file mode (100644/100755), stage (0), filesystem existence, and non-symlink status before skipping, then moves cd \"$repo_root\" to after the new checks so the rest of the script still operates at the repo root.
  • Installer script: Extends the post-install log to describe the new opt-out, its "tracked regular file" requirement, and the ECC_PREPUSH_FULL=1 one-shot override.
  • Tests: Seven new tests cover the worktree happy path, untracked marker, core.symlinks=false symlink-mode index entries, deletion-push passthrough, ECC_PREPUSH_FULL override, pre-commit secret detection regression, and the unchanged default behavior.

Confidence Score: 4/5

Safe to merge — the core hook logic is well-guarded and the new opt-out path has appropriate layered checks; the two observations are design-level and do not affect correctness.

The hook implementation is careful: it validates the index stage, file mode, filesystem type, and symlink status before skipping, and the ECC_PREPUSH_FULL=1 escape hatch is always available. Two design-level points lower confidence slightly: the opt-out triggers on staged-but-uncommitted index entries (a developer who accidentally does git add .ecc-prepush-disable via git add . will have their next push bypassed without realising it), and any committed repository can suppress the pre-push suite for all ECC users who clone it. Neither causes incorrect behaviour today, but both are worth tracking before the feature sees wide adoption.

The pre-push hook deserves a second read around the git ls-files --stage opt-out block (lines 20-32), specifically around whether an index entry at stage 0 is the right boundary for user intent.

Important Files Changed

Filename Overview
scripts/codex-git-hooks/pre-push Adds repository-scoped .ecc-prepush-disable opt-out with index mode/stage guards and symlink rejection; replaces --is-inside-work-tree guard with --show-toplevel to capture repo_root, and moves cd "$repo_root" after the new checks. Implementation is sound, but the opt-out triggers on staged (not yet committed) index entries, which may surprise developers.
scripts/codex/install-global-git-hooks.sh Adds three new log lines that describe the pre-push-only opt-out and the ECC_PREPUSH_FULL=1 override; cleanly expands the existing guidance without changing any install logic.
tests/scripts/codex-hooks.test.js Adds 7 focused tests covering the happy path, untracked marker, symlink-mode marker (core.symlinks=false), deletion push with ECC_PREPUSH_FULL, pre-commit still blocking secrets, and default-unchanged behavior; helper functions runGit, runHook, createLinkedWorktreeFixture, and cleanupLinkedWorktreeFixture are well-isolated and tidy.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[git push triggered] --> B{ECC_SKIP_GIT_HOOKS=1\nor ECC_SKIP_PREPUSH=1?}
    B -- yes --> Z[exit 0]
    B -- no --> C{git rev-parse\n--show-toplevel succeeds?}
    C -- no --> Z
    C -- yes --> D{.ecc-hooks-disable\nor git_dir/ecc-hooks-disable\nexists?}
    D -- yes --> Z
    D -- no --> E[Read git index entry\nfor .ecc-prepush-disable\nvia ls-files --stage]
    E --> F{ECC_PREPUSH_FULL=1?}
    F -- yes --> I
    F -- no --> G{File on disk,\nnot a symlink,\nstage==0,\nmode 100644 or 100755?}
    G -- yes --> H[Print opt-out message\nexit 0]
    G -- no --> I[cd repo_root]
    I --> J{All refs are\ndeletion pushes?}
    J -- yes --> Z
    J -- no --> K[Run lint / typecheck /\ntest / build / go test / pytest]
    K --> L{All checks pass?}
    L -- yes --> M[exit 0]
    L -- no --> N[fail + exit 1]
Loading

Reviews (1): Last reviewed commit: "fix: validate pre-push marker index mode" | Re-trigger Greptile

Comment on lines +23 to +26
prepush_marker_entry="$(git -C "$repo_root" ls-files --stage -- ".ecc-prepush-disable" 2>/dev/null || true)"
if [[ -n "$prepush_marker_entry" ]]; then
read -r prepush_marker_mode _prepush_marker_oid prepush_marker_stage _prepush_marker_path <<< "$prepush_marker_entry"
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Staged-but-uncommitted marker silently disables pre-push checks

git ls-files --stage reflects the index, not committed history. If a developer runs git add .ecc-prepush-disable (e.g., via git add .) without yet committing, the very next git push will be bypassed with the opt-out message, even though the file has never been committed and the developer may not realize what happened. The installer guidance says "creating and tracking a regular .ecc-prepush-disable file" which most developers interpret as requiring a commit. Consider adding a comment here noting that staged-but-uncommitted entries are also accepted, and potentially document this in the installer log.

Comment on lines +27 to +32
if [[ "${ECC_PREPUSH_FULL:-0}" != "1" && -f "$prepush_marker" && ! -L "$prepush_marker" ]] \
&& [[ "$prepush_marker_stage" == "0" ]] \
&& [[ "$prepush_marker_mode" == "100644" || "$prepush_marker_mode" == "100755" ]]; then
printf '[ECC pre-push] Skipping: repository opt-out .ecc-prepush-disable is tracked; CI/remote checks remain authoritative.\n'
exit 0
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Any cloned repository can suppress a developer's entire pre-push suite

When a developer clones a repository that has .ecc-prepush-disable committed at the root, ECC will silently skip lint, typecheck, test, and build on every subsequent push from that repo. The log message is a one-liner that appears in push output and is easy to miss, especially in CI-like terminal environments or automated push scripts. Per SECURITY.md, "Local developer tools can still be valid security issues when untrusted repository content... can trigger execution without clear user intent." Here the effect is suppression rather than execution, but the net result is the same: a third-party repository author can modify a developer's local security posture without explicit per-user consent. Pre-commit checks remain active, which limits exposure, but the gap is worth documenting in a user-facing security note.

Rule Used: Treat CLI inputs, URLs, file paths, and subprocess... (source)

@daltino daltino left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

DECISION: APPROVE

This PR improves the pre-push hook functionality by adding repository-scoped opt-out options, such as .ecc-prepush-disable, making the workflow more flexible. The changes respect existing disabling methods and add clear documentation for opting out. The tests also cover the new functionality comprehensively, ensuring robustness. Everything aligns well with the repo's contribution guidelines, particularly around enhancing hooks and adding utility for developers.

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.

2 participants