Skip to content

feat(hooks): capture git_remote_url for Repo Allowlist (claude-code + copilot)#192

Open
MohamedAklamaash wants to merge 7 commits into
mainfrom
feature/repo-allowlist
Open

feat(hooks): capture git_remote_url for Repo Allowlist (claude-code + copilot)#192
MohamedAklamaash wants to merge 7 commits into
mainfrom
feature/repo-allowlist

Conversation

@MohamedAklamaash

@MohamedAklamaash MohamedAklamaash commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Client hooks capture the working repo's git remote and send it to the gateway so the Repo Allowlist policy can be enforced. v1 tools: claude-code + copilot.

Changes

  • _get_git_context(cwd): one git config --get remote.origin.url subprocess (2s timeout), credentials stripped (scheme and scp form, last-@), cached per (session_id, cwd), never crashes the hook (null on no-git/no-remote/timeout).
  • git_remote_url added to both user_prompt and pre_tool_use bodies in claude-code and copilot unbound.py; helper byte-for-byte identical across both copies. Backward-compatible (null/absent → unchanged shape). git via subprocess only (Zscaler curl rule untouched).

Notes

Copilot's UserPromptSubmit cwd is unconfirmed against a live transcript — handled safely (passes through, null if absent; pre-tool-use path always works). Follow-up: confirm Copilot prompt-submit payload.

Tests

16 tests / 40 subtests ✓ — credential-strip matrix (scheme/scp/@-in-password/ssh-port), no-git→None, timeout→None, caching (1 shell-out for 2 calls), 4-body parity.


Threat model (honest labeling): careless-insider control. Repo identity is client-supplied git context and is trivially bypassable by rewriting origin or going direct-to-provider — a deter/visibility control, not a hard security boundary. Default audit; block is opt-in per policy.

Part of a 4-PR feature (Repo Allowlist): ai-gateway-data (policy type) · ai-gateway (enforcement) · setup (hook capture) · unbound-fe (editor).

🤖 Generated with Claude Code


Note

Medium Risk
Changes the policy pre-check payload on a latency-sensitive path (subprocess + caching) across three integrations; repo identity is client-supplied and bypassable, though failures degrade to null without blocking the hook.

Overview
Adds Repo Allowlist support by resolving the working repository’s credential-stripped origin URL and sending it as git_remote_url on pre-tool-use gateway requests in claude-code, copilot, and cursor hooks.

New helpers run a bounded git config lookup (2s timeout, per-(session_id, cwd) cache), strip embedded credentials from remotes, and pick the repo directory via _repo_context_dir (target file’s nearest existing ancestor, so new paths still map to the enclosing repo). Claude Code also treats MultiEdit / NotebookEdit as native file tools and unifies path extraction with _tool_file_path (file_path / notebook_path). Copilot can derive the target path from apply_patch patch text when building metadata.

test_repo_allowlist.py covers stripping, caching, repo-dir resolution, payload parity, and approval-retry behavior (git lookup skipped on retry).

Reviewed by Cursor Bugbot for commit 122e7ed. Bugbot is set up for automated code reviews on this repo. Configure here.


Linear: WEB-5002 — Check the cwd and allow AI tools only for organization repositories.

Greptile Summary

This PR captures the working repository's git remote URL (git config --get remote.origin.url) in the pre-tool-use hook path for claude-code, copilot, and cursor, attaching a credential-stripped git_remote_url field to each gateway request so the Repo Allowlist policy can be enforced server-side.

  • _get_git_context / _strip_git_credentials / _repo_context_dir helpers are added identically to all three hooks; results are cached per (session_id, cwd) with a 256-entry FIFO bound; transient subprocess failures log an error and skip caching, avoiding the poison-cache problem.
  • claude-code additionally widens NATIVE_FILE_TOOLS to include MultiEdit and NotebookEdit, with a new _tool_file_path helper that normalises file_path/notebook_path extraction.
  • test_repo_allowlist.py (16 test classes, 40+ subtests) covers credential-strip edge cases, caching, ancestor-dir walk, subdir repo resolution, approval-retry bypass, and payload parity across all three hooks.

Confidence Score: 4/5

The change is fail-open (null on any error), credential-stripping is thoroughly tested, and the approval-retry early return correctly bypasses the git subprocess. The one gap is that cursor/unbound.py ships the new subprocess and cache path with zero instrumentation, matching the already-noted absence in claude-code and copilot.

The core logic is sound and the test suite is thorough, but the cursor hook introduces the same unmonitored subprocess/cache flow that was flagged for claude-code and copilot — a regression there (git binary unreachable, wrong remote resolved) would be invisible in production with no latency histogram, counter, or cache-hit metric to surface it.

cursor/unbound.py — new _get_git_context flow has no Prometheus metrics; same instrumentation gap as claude-code and copilot but not yet flagged for cursor.

Important Files Changed

Filename Overview
claude-code/hooks/unbound.py Adds _get_git_context / _strip_git_credentials / _repo_context_dir helpers plus bounded cache; extends NATIVE_FILE_TOOLS to include MultiEdit and NotebookEdit; injects git_remote_url into pre-tool-use request body after the approval-retry early-return path. Missing observability metrics were flagged in a prior review thread.
copilot/hooks/unbound.py Byte-for-byte identical helpers to claude-code; additionally adds _extract_patch_target_path to the file_path resolution chain so apply_patch operations resolve to the correct repo. Missing observability metrics were flagged in a prior review thread.
cursor/unbound.py Same helpers as claude-code/copilot added here; git_remote_url injected using conversation_id as cache key. No Prometheus metrics for the new subprocess/cache flow — flagged as a P1 finding.
test_repo_allowlist.py New test module covering credential stripping edge cases, caching behaviour (including bounded eviction and transient-failure non-caching), repo-dir ancestor walk, subdir-repo resolution, approval-retry skips, apply_patch path extraction, and payload parity across all three hooks.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[process_pre_tool_use] --> B{is_retry?}
    B -- yes, marker_data found --> C[poll_approval_status]
    C --> D[return early — no git lookup]
    B -- no, or marker_data missing --> E[build request_body]
    E --> F[_tool_file_path / file_path extraction]
    F --> G[_repo_context_dir\ncwd + file_path → nearest existing dir]
    G --> H{cache hit?\nkey = session_id × cwd}
    H -- yes --> I[return cached URL or None]
    H -- no --> J[subprocess: git -C cwd config --get remote.origin.url\ntimeout=2s]
    J -- exception\nTimeout / FileNotFoundError --> K[log_error, return None\nnot cached]
    J -- returncode != 0 --> L[result = None\ncache entry written]
    J -- returncode == 0 --> M[_strip_git_credentials\nremove userinfo from URL]
    M --> N[result = stripped URL\ncache entry written]
    L --> O[request_body.git_remote_url = result]
    N --> O
    I --> O
    O --> P[send_to_hook_api]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[process_pre_tool_use] --> B{is_retry?}
    B -- yes, marker_data found --> C[poll_approval_status]
    C --> D[return early — no git lookup]
    B -- no, or marker_data missing --> E[build request_body]
    E --> F[_tool_file_path / file_path extraction]
    F --> G[_repo_context_dir\ncwd + file_path → nearest existing dir]
    G --> H{cache hit?\nkey = session_id × cwd}
    H -- yes --> I[return cached URL or None]
    H -- no --> J[subprocess: git -C cwd config --get remote.origin.url\ntimeout=2s]
    J -- exception\nTimeout / FileNotFoundError --> K[log_error, return None\nnot cached]
    J -- returncode != 0 --> L[result = None\ncache entry written]
    J -- returncode == 0 --> M[_strip_git_credentials\nremove userinfo from URL]
    M --> N[result = stripped URL\ncache entry written]
    L --> O[request_body.git_remote_url = result]
    N --> O
    I --> O
    O --> P[send_to_hook_api]
Loading

Reviews (5): Last reviewed commit: "fix(repo-allowlist): harden git-context ..." | Re-trigger Greptile

Context used:

  • Context used - P0 — Critical (must block merge)
    Django / Backend ... (source)

MohamedAklamaash and others added 2 commits June 30, 2026 00:01
…po Allowlist client-hook)

Capture the credential-stripped origin remote URL for the event cwd via a
short-timeout `git config --get remote.origin.url` subprocess, cached per
(session_id, cwd), and inject it as `git_remote_url` into both the user_prompt
and pre_tool_use bodies. Never raises; null when not a git repo, no origin,
git missing/timeout, or no cwd. Copilot UserPromptSubmit cwd is unverified
in-repo, so its prompt-submit path reads event.get('cwd') defensively.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_strip_git_credentials only stripped scheme:// URLs via first-@, so scp-form
remotes (user:token@host:org/repo) leaked credentials on the wire and an @ in
the password left a fragment. Rewrite to strip userinfo using the last @ before
the path boundary for both scheme and scp forms; total (never raises). Helper
kept byte-for-byte identical across claude-code and copilot. Extends the
credential test matrix (scp-with-token, @-in-password, ssh port).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@MohamedAklamaash MohamedAklamaash requested a review from a team June 29, 2026 19:33
Comment thread claude-code/hooks/unbound.py Outdated
Comment on lines +961 to +981
def _get_git_context(session_id: Optional[str], cwd: Optional[str]) -> Optional[str]:
"""Credential-stripped origin remote URL for cwd, or None. Cached per
(session_id, cwd); never raises."""
key = (session_id, cwd)
if key in _GIT_CONTEXT_CACHE:
return _GIT_CONTEXT_CACHE[key]
result = None
if cwd:
try:
out = subprocess.run(
['git', '-C', cwd, 'config', '--get', 'remote.origin.url'],
capture_output=True, text=True, timeout=2,
)
if out.returncode == 0:
url = out.stdout.strip()
if url:
result = _strip_git_credentials(url)
except Exception:
result = None
_GIT_CONTEXT_CACHE[key] = result
return result

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Missing observability metrics for the git context capture flow

_get_git_context introduces a new subprocess call path with caching but ships zero Prometheus metrics. Per the team's instrumentation requirement, every new flow must be instrumented now regardless of dashboard use. At minimum this flow needs: (1) a latency histogram for the subprocess.run call (labeled with outcome: hit, miss, timeout, error), (2) a success/failure counter, and (3) a cache hit/miss counter. Without these, a regression (e.g., a git binary path change causing silent None for all users) would be completely invisible in production. The same gap exists identically in copilot/hooks/unbound.py.

Context Used: P0 — Critical (must block merge)
Django / Backend ... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Intentional — these are client-side hooks that run as short-lived subprocesses inside the user's editor (Claude Code / Copilot / Cursor), not a long-running server. There is no Prometheus registry, scrape endpoint, or process for a collector to reach, so a histogram/counter would have nothing to expose. The server-side instrumentation requirement doesn't apply to this runtime. Observability for this flow is handled the way the rest of the hook is: _get_git_context now emits a log_error (category git_context, fire-and-forward to the gateway error sink) on the failure path, which is the signal a 'git binary path change → silent None' regression would surface through.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

That's a valid correction — the client-side subprocess runtime makes Prometheus metrics inapplicable here, and the log_error path to the gateway error sink is the right observability primitive for this context. Withdrawing this comment.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

Comment thread claude-code/hooks/unbound.py Outdated
Comment on lines +978 to +979
except Exception:
result = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Exceptions silently swallowed with no diagnostic log

The bare except Exception: result = None swallows every failure — FileNotFoundError (git not on PATH), TimeoutExpired, permission errors — without emitting any log entry. If _get_git_context starts returning None in production due to a misconfiguration or environment issue, there is no signal in the logs to investigate. The same silent-swallow pattern exists in copilot/hooks/unbound.py at the equivalent line. At minimum, a log_error (or equivalent) with session_id and cwd should be emitted on the exception branch so failures are reconstructable.

Context Used: P0 — Critical (must block merge)
Django / Backend ... (source)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 122e7ed — the subprocess call is now wrapped so the exception branch emits log_error("git context lookup failed session=… cwd=…: <exc>") (category git_context) before returning None. Control flow is unchanged; failures are now reconstructable. Mirrored across all three hooks.

Comment thread claude-code/hooks/unbound.py Outdated
Comment thread claude-code/hooks/unbound.py

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🛡️ Automated Security Review (consensus)

1 finding — 0 high-confidence, 1 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.

Findings

🟡 TRIAGE — _strip_git_credentials can leave credentials in git_remote_url

claude-code/hooks/unbound.py:943 · copilot/hooks/unbound.py:925

Impact: Authority is truncated at the first / before locating @; a hand-set remote with a literal / in userinfo (e.g. https://user:p/ss@github.com/org/repo) returns the URL unchanged and may send/store the password in gateway audit records.

Fix: Resolve @ on the full credential span before splitting on /, or fail closed (return None/redact) when @ remains after stripping; add a /-in-password test case.

Flagged by: Claude, Cursor (lead)


Previously acknowledged (not re-flagged)

  • Client-supplied git_remote_url as an enforcement boundary — PR threat model: careless-insider deter/visibility control, trivially bypassable via rewritten origin or direct provider access; default audit, block opt-in. Accepted by design.

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head df98388a · 2026-06-29T19:44Z

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread copilot/hooks/unbound.py Outdated
@vigneshsubbiah16

Copy link
Copy Markdown
Collaborator

🛡️ Automated Security Review (consensus)

2 findings — 2 high-confidence, 0 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.

Credential sanitizer fails open on parse errors — 🔴 HIGH

claude-code/hooks/unbound.py:954 · copilot/hooks/unbound.py:941

  • Impact: _strip_git_credentials returns the unsanitized URL on any exception, so a malformed/unusual remote.origin.url can forward embedded tokens in git_remote_url to the gateway and logs.
  • Fix: Fail closed — return None on exception and drop any URL that still contains @ in the authority after stripping.
  • Reviewers: Claude, Lead

Copilot apply_patch skips patch target for repo resolution — 🔴 HIGH

copilot/hooks/unbound.py:1078

  • Impact: apply_patch is canonicalized as Edit but its target path lives inside the patch payload; _repo_context_dir only reads filePath/path/file_path, so nested-repo patches can report wrong or null git_remote_url and weaken Repo Allowlist attribution.
  • Fix: Parse the target file path from apply_patch/patch payload fields before calling _repo_context_dir.
  • Reviewers: Cursor (Bugbot), Lead

Previously acknowledged (not re-flagged)

  • Client-supplied git_remote_url is bypassable — PR threat model: deter/visibility control for careless insiders, not a hard security boundary.
  • Subprocess on pre-tool path with session-scoped caching (incl. timeout/error → cached null) — Accepted design: 2s timeout, never crashes the hook; failures degrade to null.
  • Missing metrics, silent exception logging, unbounded cache — Greptile reliability/observability notes; not re-flagged as security-boundary issues in this pass.
  • Semgrep insecure-file-permissions hits — Pre-existing chmod usage on unchanged lines outside this diff; out of scope for this PR.

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 6b926f1a · 2026-06-30T04:04Z

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7849e78. Configure here.

Comment thread claude-code/hooks/unbound.py Outdated
@vigneshsubbiah16

Copy link
Copy Markdown
Collaborator

🛡️ Automated Security Review (consensus)

2 findings — 2 high-confidence, 0 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.

Credential sanitizer fails open on parse errors — 🔴 HIGH

claude-code/hooks/unbound.py:957-958 (also copilot/hooks/unbound.py:939, cursor/unbound.py:682)

  • Impact: Any exception in _strip_git_credentials returns the original remote.origin.url, so embedded userinfo can be POSTed to the gateway and cached for the session.
  • Fix: On parse failure return None (or a host-only fallback), never the raw input; add a regression test that forces the exception path.
  • Reviewers: Cursor, Claude

Credential sanitizer leaks when password contains / — 🔴 HIGH

claude-code/hooks/unbound.py:945-955 (also copilot/hooks/unbound.py:923, cursor/unbound.py:666)

  • Impact: Authority is split on the first / before locating @, so URLs like https://user:to/ken@github.com/... pass through with credentials intact; test matrix does not cover this case.
  • Fix: Parse with urllib.parse.urlsplit (drop username/password) or locate @ on the full authority segment; add tests for / and %2F in passwords.
  • Reviewers: Claude

Previously acknowledged (not re-flagged)

  • Client-supplied git_remote_url is spoofable — Accepted in the PR threat model as a visibility/deterrence control, not a hard security boundary.

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 7849e780 · 2026-06-30T05:23Z

MohamedAklamaash and others added 2 commits June 30, 2026 11:42
…hook

MultiEdit and NotebookEdit mutate files but were absent from the
claude-code native-file-tool set, so edits through them skipped the
git_remote_url stamp and never reached the repo-allowlist gate. Add both
to ALLOWED_NON_MCP_HOOK_NAMES and NATIVE_FILE_TOOLS, and extract the
target path (file_path for MultiEdit, notebook_path for NotebookEdit) so
the gate resolves the governing repo exactly like Edit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assert _tool_file_path returns None for empty/missing file_path so the
file-tool repo resolution never treats a blank path as a target.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MohamedAklamaash

Copy link
Copy Markdown
Contributor Author

Update (repo-allowlist hook parity):

  • Claude Code hook now recognizes MultiEdit/NotebookEdit as native file tools (added to ALLOWED_NON_MCP_HOOK_NAMES + NATIVE_FILE_TOOLS); new _tool_file_path resolves file_path (Edit/MultiEdit) or notebook_path (NotebookEdit) — closes a real NotebookEdit allowlist-evasion gap, in parity with the gateway.
  • Tests cover both tools + negative shapes. CI green.

@vigneshsubbiah16

Copy link
Copy Markdown
Collaborator

🛡️ Automated Security Review (consensus)

2 findings — 1 high-confidence, 1 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.

Credential sanitizer fails open on parse errors — 🔴 HIGH

claude-code/hooks/unbound.py:966 · copilot/hooks/unbound.py:948 · cursor/unbound.py:691
Impact: _strip_git_credentials catches all exceptions and returns the original URL; a credential-bearing remote.origin.url could leak userinfo to the gateway/logs and be cached for the session.
Fix: Fail closed — return None on exception, never forward the raw input.
Flagged by: Cursor, Claude

Copilot apply_patch omits target path for repo resolution — 🟡 TRIAGE

copilot/hooks/unbound.py:1078
Impact: apply_patch is canonicalized to Edit but its target path only appears inside the patch payload; _repo_context_dir falls back to session cwd, so nested-repo edits may send wrong/null git_remote_url and weaken allowlist matching.
Fix: Extract the patch target path before calling _repo_context_dir (mirror claude-code _tool_file_path / path-resolution patterns).
Flagged by: Cursor

Previously acknowledged (not re-flagged)

  • Client-supplied git_remote_url is bypassable — deter/visibility control, not a hard security boundary; default audit mode (PR threat model).
  • Fail-open to null on no-git / no-remote / timeout; hooks never crash — accepted hook design (PR description).
  • MultiEdit / NotebookEdit hook coverage — addressed as allowlist strengthening, not a weakening (@MohamedAklamaash).

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 4a8021e9 · 2026-06-30T09:38Z

- Fail closed in _strip_git_credentials: return None on parse failure
  instead of the raw credential-bearing URL (no userinfo leak to gateway).
- Do not cache transient git-subprocess failures; log them at debug level
  with session/cwd. Bound _GIT_CONTEXT_CACHE to 256 entries (FIFO evict).
- Skip the git-context lookup on the Slack approval-retry path whose body
  is never sent.
- copilot: resolve apply_patch repo identity from the patch payload path.

Mirrored across claude-code, copilot, and cursor hooks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MohamedAklamaash

Copy link
Copy Markdown
Contributor Author

Review dispositions — addressed in 122e7ed

All changes mirrored across the three client hooks (claude-code/hooks/unbound.py, copilot/hooks/unbound.py, cursor/unbound.py) to keep them in parity. Focused regression tests added in test_repo_allowlist.py (28 passed / 91 subtests).

Fixed

  • High — sanitizer returns raw URL on error (3496327853): _strip_git_credentials now fails closed (returns None on parse failure, never the credential-bearing URL).
  • Med — apply_patch misses repo context (3496037025): copilot now resolves the target path from the patch payload so nested-repo identity resolves.
  • Med — git runs on approval retry (3494248466): git_remote_url is computed after the early-returning retry branch, only when the body is actually sent.
  • P2 — transient timeout poisons cache (3494256384): transient failures are no longer cached; only successful / conclusive-no-repo lookups are.
  • P2 — unbounded cache (3494256470): _GIT_CONTEXT_CACHE capped at 256 entries with FIFO eviction.
  • P2 — silently swallowed exceptions (3494256286): failure path now emits log_error with session/cwd; control flow unchanged.

Intentional (no change)

  • P1 — no Prometheus metrics for git context (3494256176): these are short-lived client-side hook subprocesses in the user's editor, not a server with a metrics registry/scrape endpoint — server-metrics expectations don't apply. Failure observability is via the existing log_error gateway error sink.

@vigneshsubbiah16

Copy link
Copy Markdown
Collaborator

🛡️ Automated Security Review (consensus)

2 findings — 1 high-confidence, 1 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.

Credential sanitizer leaks userinfo when password contains / — 🔴 HIGH

claude-code/hooks/unbound.py:968, copilot/hooks/unbound.py:950, cursor/unbound.py:693

Impact: _strip_git_credentials truncates the authority at the first /; a literal / in the password splits before @, hits at == -1, and returns the original credential-bearing URL (no exception → fail-closed path never runs), which is cached and sent as git_remote_url.

Fix: Bound userinfo on the full pre-path segment (last @ before /, ?, or #), or use urllib.parse.urlsplit and strip username/password; return None (not the raw URL) when stripping fails; add a regression test for user:p/w@host/....

Reviewers: Claude, Cursor


Permissive file permissions on hook install paths — 🟡 TRIAGE

claude-code/hooks/unbound.py:1515, claude-code/hooks/unbound.py:1807, copilot/hooks/unbound.py:1481, copilot/hooks/unbound.py:1700

Impact: chmod uses $BITS or 0o755, granting broader filesystem access than 0o644 on paths Semgrep flags as sensitive (pre-existing install logic, unchanged in this PR).

Fix: If these paths are not meant to be world-readable/executable, tighten to owner-only (0o700/0o600); otherwise document as intentional for hook deployment.

Reviewers: Semgrep


Previously acknowledged (not re-flagged)

  • Sanitizer fail-open on parse exception — fixed in 122e7ed: returns None on parse failure, never forwards credential-bearing URL (@MohamedAklamaash).
  • Copilot apply_patch misses repo context — fixed in 122e7ed: resolves target path from patch payload (@MohamedAklamaash).
  • Git subprocess on approval-retry path — fixed in 122e7ed: git_remote_url computed only when the body is actually sent (@MohamedAklamaash).
  • Transient timeout poisons cache — fixed in 122e7ed: transient failures not cached (@MohamedAklamaash).
  • Unbounded _GIT_CONTEXT_CACHE — fixed in 122e7ed: capped at 256 entries with FIFO eviction (@MohamedAklamaash).
  • Exceptions swallowed without logging — fixed in 122e7ed: log_error on subprocess failure (@MohamedAklamaash).
  • Missing Prometheus metrics for git context — intentional: short-lived client-side hook subprocesses have no metrics registry; failures surface via log_error gateway sink (@MohamedAklamaash).
  • Client-supplied repo identity is bypassable — accepted threat model: deterrence/visibility control, not a hard security boundary (PR description).

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 122e7ed1 · 2026-06-30T10:58Z

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