Skip to content

Add header Git branch dropdown#121

Merged
friuns2 merged 2 commits into
mainfrom
codex/header-git-commit-dropdown
May 3, 2026
Merged

Add header Git branch dropdown#121
friuns2 merged 2 commits into
mainfrom
codex/header-git-commit-dropdown

Conversation

@friuns2
Copy link
Copy Markdown
Owner

@friuns2 friuns2 commented May 3, 2026

Summary

  • replace the simple header branch/review action with a searchable Git dropdown
  • add branch switching, detached commit checkout, recent commit expansion, and active commit marking
  • add guarded backend Git endpoints, dark-theme styling, and manual test documentation

Tests

  • pnpm run build:frontend
  • pnpm run build:cli
  • git diff --check

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Add header Git branch dropdown with commit checkout

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Replace simple header branch action with searchable Git dropdown component
• Add detached HEAD state display with commit subject, SHA, and date metadata
• Implement branch commit expansion showing recent commits with checkout capability
• Add backend endpoints for branch commits and detached commit checkout with worktree validation
• Extend GitBranchState type with HEAD metadata and dirty/detached status tracking
• Apply dark theme styling to new dropdown component and related elements
Diagram
flowchart LR
  A["Git Branch State API"] -->|headSha, headSubject, headDate, detached, dirty| B["HeaderGitBranchDropdown Component"]
  C["Branch Commits Endpoint"] -->|GitCommitOption list| B
  D["Checkout Commit Endpoint"] -->|POST with SHA| B
  B -->|Display current branch or detached HEAD| E["Header Trigger"]
  B -->|Search & filter branches| F["Branch List"]
  B -->|Expand & show commits| G["Commit List"]
  E -->|Toggle Review pane| H["Review State"]
  F -->|Checkout branch| I["Git Checkout"]
  G -->|Checkout commit| J["Git Detached Checkout"]
Loading

Grey Divider

File Changes

1. src/api/codexGateway.ts ✨ Enhancement +84/-3

Extend Git state types and add commit checkout APIs

• Extended WorktreeBranchOption type with isCurrent and isRemote boolean flags
• Extended GitBranchState type with headSha, headSubject, headDate, detached, dirty, and
 gitRoot fields
• Added new GitCommitOption type for commit data with sha, shortSha, subject, and date
• Added getGitBranchCommits() function to fetch recent commits for a branch
• Added checkoutGitCommit() function to checkout a specific commit SHA with detached HEAD support
• Updated getGitBranchState() to populate all new fields and mark current branch with isCurrent
 flag

src/api/codexGateway.ts


2. src/server/codexAppServerBridge.ts ✨ Enhancement +118/-14

Add Git header state endpoints and commit checkout support

• Added readGitHeaderState() helper to capture current branch, HEAD SHA, commit subject, date,
 detached state, and dirty status
• Added assertCleanGitWorktree() guard to prevent checkout operations with uncommitted changes
• Updated /codex-api/git/branches endpoint to include isCurrent and isRemote flags and return
 full header state
• Added /codex-api/git/branch-commits GET endpoint to fetch up to 12 recent commits for a branch
• Added /codex-api/git/checkout-commit POST endpoint to checkout a specific commit with detached
 HEAD support
• Enhanced branch activity tracking to store both timestamp and remote flag metadata

src/server/codexAppServerBridge.ts


3. src/App.vue ✨ Enhancement +123/-46

Integrate HeaderGitBranchDropdown and manage Git state

• Replaced ComposerDropdown with new HeaderGitBranchDropdown component for branch/commit
 management
• Added reactive state for HEAD metadata: currentThreadHeadSha, currentThreadHeadSubject,
 currentThreadHeadDate
• Added reactive state for detached HEAD and dirty worktree tracking: isThreadDetachedHead,
 isThreadWorktreeDirty
• Added reactive state for branch commits: threadBranchCommitsByBranch,
 threadBranchCommitsLoadingFor, threadBranchCommitsError
• Added threadBranchError state for error messaging without browser alerts
• Implemented loadThreadBranchCommits() to lazy-load commits on branch expansion
• Implemented onCheckoutContentHeaderCommit() to handle detached commit checkout
• Implemented applyThreadGitState() helper to sync full Git state from API responses
• Updated loadThreadBranches() to populate all new state fields
• Removed old computed properties contentHeaderBranchDropdownValue,
 contentHeaderBranchDropdownOptions, contentHeaderBranchDropdownIcon
• Removed IconTablerGitFork import (moved to HeaderGitBranchDropdown)

src/App.vue


View more (3)
4. src/components/content/HeaderGitBranchDropdown.vue ✨ Enhancement +358/-0

New searchable Git branch and commit dropdown component

• New component providing searchable Git branch dropdown with commit expansion
• Displays current branch or detached HEAD state with commit subject, SHA, and date metadata
• Shows dirty worktree indicator dot and status message
• Supports branch search/filtering with keyboard escape handling
• Expandable branch rows showing up to 12 recent commits per branch
• Marks current commit and branch with visual indicators
• Emits events for Review toggle, branch checkout, and commit checkout
• Lazy-loads commits on branch expansion via loadCommits event
• Includes click-outside detection to close dropdown
• Full keyboard navigation support with search input focus management

src/components/content/HeaderGitBranchDropdown.vue


5. src/style.css ✨ Enhancement +67/-0

Add dark theme styling for Git dropdown component

• Added dark theme styles for .header-git-trigger, .header-git-menu, and all child elements
• Styled dark theme states for review-open, hover, current, and error states
• Applied dark theme colors to branch buttons, commit rows, state display, and search input
• Styled dark theme badges, metadata text, and error messages
• Ensured consistent color contrast and visual hierarchy in dark mode

src/style.css


6. tests.md 📝 Documentation +37/-0

Add manual test documentation for Git dropdown

• Added comprehensive manual test documentation for header Git branch dropdown feature
• Covers light and dark theme validation
• Tests branch search, switching, commit expansion, and checkout workflows
• Validates detached HEAD state display and recovery
• Tests dirty worktree error handling and messaging
• Includes prerequisites, step-by-step instructions, expected results, and cleanup guidance

tests.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented May 3, 2026

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (0)

Grey Divider


Action required

1. Unborn repo breaks header state 🐞 Bug ☼ Reliability
Description
readGitHeaderState() unconditionally runs git commands against HEAD (rev-parse/show), which fail for
repositories with no initial commit, causing /codex-api/git/branches and /codex-api/git/checkout to
return 500 instead of a usable state.
Code

src/server/codexAppServerBridge.ts[R2510-2516]

+  const gitRoot = await runCommandCapture('git', ['rev-parse', '--show-toplevel'], { cwd })
+  const currentBranchRaw = await runCommandCapture('git', ['branch', '--show-current'], { cwd: gitRoot })
+  const currentBranch = currentBranchRaw.trim() || null
+  const headShaRaw = await runCommandCapture('git', ['rev-parse', '--short=12', 'HEAD'], { cwd: gitRoot })
+  const headCommitRaw = await runCommandCapture('git', ['show', '-s', '--date=short', '--format=%cd%x09%s', 'HEAD'], { cwd: gitRoot })
+  const [headDate = '', ...headSubjectParts] = headCommitRaw.split('\t')
+  const statusRaw = await runCommandCapture('git', ['status', '--porcelain'], { cwd: gitRoot })
Evidence
The server already has isMissingHeadError() for unborn repos, but readGitHeaderState() doesn’t use
it and will reject on HEAD-related commands. The /codex-api/git/branches and /codex-api/git/checkout
handlers call readGitHeaderState(), so unborn repos will consistently error for the new dropdown.

src/server/codexAppServerBridge.ts[2439-2445]
src/server/codexAppServerBridge.ts[2501-2525]
src/server/codexAppServerBridge.ts[5857-5935]
src/server/codexAppServerBridge.ts[5979-6024]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`readGitHeaderState()` assumes `HEAD` resolves to a commit and fails in an unborn repository (no commits). This turns valid repos into 500s for `/codex-api/git/branches` and for successful checkouts that still have no commits.

### Issue Context
`isMissingHeadError()` already exists and is used elsewhere to handle missing-HEAD cases; the header state path should do the same.

### Fix Focus Areas
- src/server/codexAppServerBridge.ts[2501-2525]
- src/server/codexAppServerBridge.ts[2439-2445]

### Implementation notes
- Wrap `rev-parse ... HEAD` and `git show ... HEAD` in `try/catch`.
- If the error matches `isMissingHeadError`, return `headSha/headSubject/headDate = null` (and keep `currentBranch` from `git branch --show-current`, plus `dirty` from `git status`).
- Only rethrow for non-missing-HEAD errors.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Git option injection via refs 🐞 Bug ⛨ Security
Description
The new /codex-api/git/branch-commits and /codex-api/git/checkout-commit endpoints pass
user-controlled branch/sha into git argv without validating that they are safe refs/hex SHAs;
values starting with '-' can be parsed as git options, leading to unintended command behavior.
Code

src/server/codexAppServerBridge.ts[R6027-6084]

+      if (req.method === 'GET' && url.pathname === '/codex-api/git/branch-commits') {
+        const rawCwd = (url.searchParams.get('cwd') ?? '').trim()
+        const branch = (url.searchParams.get('branch') ?? '').trim()
+        if (!rawCwd) {
+          setJson(res, 400, { error: 'Missing cwd' })
+          return
+        }
+        if (!branch) {
+          setJson(res, 400, { error: 'Missing branch' })
+          return
+        }
+        const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd)
+        try {
+          const gitRoot = await runCommandCapture('git', ['rev-parse', '--show-toplevel'], { cwd })
+          await runCommandCapture('git', ['rev-parse', '--verify', `${branch}^{commit}`], { cwd: gitRoot })
+          const output = await runCommandCapture(
+            'git',
+            ['log', '-n', '12', '--date=short', '--format=%H%x09%h%x09%cd%x09%s', branch],
+            { cwd: gitRoot },
+          )
+          const commits = output.split('\n').flatMap((line) => {
+            const [sha = '', shortSha = '', date = '', ...subjectParts] = line.split('\t')
+            const subject = subjectParts.join('\t').trim()
+            return sha.trim() && shortSha.trim()
+              ? [{ sha: sha.trim(), shortSha: shortSha.trim(), date: date.trim(), subject: subject || shortSha.trim() }]
+              : []
+          })
+          setJson(res, 200, { data: commits })
+        } catch (error) {
+          setJson(res, 500, { error: getErrorMessage(error, 'Failed to load branch commits') })
+        }
+        return
+      }
+
+      if (req.method === 'POST' && url.pathname === '/codex-api/git/checkout-commit') {
+        const payload = await readJsonBody(req)
+        const record = asRecord(payload)
+        if (!record) {
+          setJson(res, 400, { error: 'Invalid body: expected object' })
+          return
+        }
+        const rawCwd = readNonEmptyString(record.cwd)
+        const sha = readNonEmptyString(record.sha)
+        if (!rawCwd) {
+          setJson(res, 400, { error: 'Missing cwd' })
+          return
+        }
+        if (!sha) {
+          setJson(res, 400, { error: 'Missing commit' })
+          return
+        }
+        const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd)
+        try {
+          const gitRoot = await runCommandCapture('git', ['rev-parse', '--show-toplevel'], { cwd })
+          await assertCleanGitWorktree(gitRoot)
+          const targetSha = await runCommandCapture('git', ['rev-parse', '--verify', `${sha}^{commit}`], { cwd: gitRoot })
+          await runCommand('git', ['checkout', '--detach', targetSha.trim()], { cwd: gitRoot })
+          setJson(res, 200, { data: await readGitHeaderState(gitRoot) })
Evidence
Both endpoints take branch from query params and sha from JSON and interpolate them into `git
rev-parse --verify ${value}^{commit} and git log ... ${branch}`. Because git parses leading-dash
args as options, these endpoints need explicit validation or conversion to full refs (refs/heads/*
or refs/remotes/*) before passing to git.

src/server/codexAppServerBridge.ts[6027-6058]
src/server/codexAppServerBridge.ts[6061-6089]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`branch` and `sha` are user-controlled and are passed directly to git commands. Even without a shell, this enables *option injection* into git (args beginning with `-`) and can trigger unexpected behavior.

### Issue Context
This affects new endpoints:
- `GET /codex-api/git/branch-commits`
- `POST /codex-api/git/checkout-commit`

### Fix Focus Areas
- src/server/codexAppServerBridge.ts[6027-6058]
- src/server/codexAppServerBridge.ts[6061-6089]

### Implementation notes
- Reject `branch` values starting with `-` or containing whitespace/control chars.
- Prefer mapping `branch` to a *full ref* and verifying via `git show-ref --verify --quiet`:
 - Try `refs/heads/${branch}` and `refs/remotes/${branch}`; require exactly one to exist.
 - Use the verified full ref when running `git log` and `git rev-parse`.
- For `sha`, require a hex pattern (e.g. `/^[0-9a-fA-F]{7,40}$/`) before `rev-parse --verify`.
- Consider returning 400 for invalid inputs instead of 500.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Commit error leaks across branches 🐞 Bug ≡ Correctness
Description
A commitsError from one branch can be shown while viewing another branch’s cached commits because
commitsError is global and expanding a cached branch does not clear it.
Code

src/components/content/HeaderGitBranchDropdown.vue[R69-72]

+            <div v-if="expandedBranch === branch.value" class="header-git-commits">
+              <div v-if="commitsLoadingFor === branch.value" class="header-git-commits-empty">Loading commits...</div>
+              <div v-else-if="commitsError" class="header-git-commits-empty is-error">{{ commitsError }}</div>
+              <button
Evidence
The dropdown shows commitsError whenever a branch is expanded (without checking which branch the
error belongs to). In App.vue, expanding a branch whose commits are already cached returns early and
does not clear threadBranchCommitsError, so an old error can remain visible while rendering a
different branch’s commits list.

src/components/content/HeaderGitBranchDropdown.vue[69-96]
src/App.vue[2934-2941]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`commitsError` is a single global string, but the UI renders it inside each expanded branch section. If one branch load fails, then the user expands another branch that is already cached, the previous error can still be displayed.

### Issue Context
- UI uses `v-else-if="commitsError"` inside the expanded branch panel.
- Loader early-returns for cached branches and doesn’t clear the error.

### Fix Focus Areas
- src/components/content/HeaderGitBranchDropdown.vue[69-96]
- src/App.vue[2934-2956]

### Implementation notes
Pick one:
- Track `threadBranchCommitsErrorByBranch: Record<string,string>` and pass the branch-specific error down.
- Or, when expanding a branch (even if cached), clear `threadBranchCommitsError` and only set it for the currently-loading branch; also render the error only when `commitsLoadingFor === branch.value` or when `expandedBranch === branch.value` *and* the error is tagged to that branch.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Commit loading state not reset 🐞 Bug ☼ Reliability
Description
When leaving the thread route, App.vue resets commitsByBranch but does not reset
commitsLoadingFor/commitsError; if navigation happens during an in-flight request, the next time the
same branch is expanded it can be blocked by the stale loading flag and/or show stale status.
Code

src/App.vue[R4001-4014]

    if (routeName !== 'thread') {
      threadBranchOptions.value = []
      currentThreadBranch.value = null
+      currentThreadHeadSha.value = null
+      currentThreadHeadSubject.value = null
+      currentThreadHeadDate.value = null
+      isThreadDetachedHead.value = false
+      isThreadWorktreeDirty.value = false
+      threadBranchCommitsByBranch.value = {}
+      threadBranchError.value = ''
      return
    }
+    threadBranchCommitsByBranch.value = {}
    void loadThreadBranches(cwd)
Evidence
The route/cwd watcher clears commit cache but leaves threadBranchCommitsLoadingFor and
threadBranchCommitsError untouched. loadThreadBranchCommits() will early-return if
threadBranchCommitsLoadingFor matches the requested branch, so a stale value can suppress
reloading after quick route transitions.

src/App.vue[3998-4016]
src/App.vue[2934-2940]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Commit-loading UI state is not fully reset when the route changes away from `thread`, which can leave stale `loading`/`error` indicators and can suppress reload attempts.

### Issue Context
`threadBranchCommitsLoadingFor` gates `loadThreadBranchCommits()` with an early return.

### Fix Focus Areas
- src/App.vue[3998-4016]
- src/App.vue[2934-2956]

### Implementation notes
- In the `routeName !== 'thread'` branch, also set:
 - `threadBranchCommitsLoadingFor.value = ''`
 - `threadBranchCommitsError.value = ''`
- Consider also resetting these when `composerCwd` changes (since the commit cache is cleared there too).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Branch load error never surfaced 🐞 Bug ◔ Observability
Description
loadThreadBranches() catches getGitBranchState() failures but never sets threadBranchError, so the
dropdown cannot display why branches failed to load (it only shows an error when threadBranchError
is set elsewhere).
Code

src/App.vue[R2847-2866]

  isLoadingThreadBranches.value = true
+  threadBranchError.value = ''
  try {
    const state = await getGitBranchState(targetCwd)
    threadBranchOptions.value = state.options
    currentThreadBranch.value = state.currentBranch
+    currentThreadHeadSha.value = state.headSha
+    currentThreadHeadSubject.value = state.headSubject
+    currentThreadHeadDate.value = state.headDate
+    isThreadDetachedHead.value = state.detached
+    isThreadWorktreeDirty.value = state.dirty
  } catch {
    threadBranchOptions.value = []
    currentThreadBranch.value = null
+    currentThreadHeadSha.value = null
+    currentThreadHeadSubject.value = null
+    currentThreadHeadDate.value = null
+    isThreadDetachedHead.value = false
+    isThreadWorktreeDirty.value = false
  } finally {
Evidence
The function resets threadBranchError to '' before the request and then has a bare catch { ... }
that discards the exception. The dropdown’s status banner is driven by props.error, so failures
during initial branch-state load become silent.

src/App.vue[2834-2868]
src/components/content/HeaderGitBranchDropdown.vue[156-157]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
When `getGitBranchState()` fails, `loadThreadBranches()` clears state but does not set `threadBranchError`, so the UI has no way to communicate the failure.

### Issue Context
`HeaderGitBranchDropdown` displays `props.error` via `statusMessage`, but `threadBranchError` is not populated on initial load failures.

### Fix Focus Areas
- src/App.vue[2834-2868]
- src/components/content/HeaderGitBranchDropdown.vue[156-157]

### Implementation notes
- Change `catch {` to `catch (error: unknown) {`.
- Set `threadBranchError.value = error instanceof Error ? error.message : 'Failed to load Git branches'`.
- Keep clearing branch/options state as you already do.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +2510 to +2516
const gitRoot = await runCommandCapture('git', ['rev-parse', '--show-toplevel'], { cwd })
const currentBranchRaw = await runCommandCapture('git', ['branch', '--show-current'], { cwd: gitRoot })
const currentBranch = currentBranchRaw.trim() || null
const headShaRaw = await runCommandCapture('git', ['rev-parse', '--short=12', 'HEAD'], { cwd: gitRoot })
const headCommitRaw = await runCommandCapture('git', ['show', '-s', '--date=short', '--format=%cd%x09%s', 'HEAD'], { cwd: gitRoot })
const [headDate = '', ...headSubjectParts] = headCommitRaw.split('\t')
const statusRaw = await runCommandCapture('git', ['status', '--porcelain'], { cwd: gitRoot })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Unborn repo breaks header state 🐞 Bug ☼ Reliability

readGitHeaderState() unconditionally runs git commands against HEAD (rev-parse/show), which fail for
repositories with no initial commit, causing /codex-api/git/branches and /codex-api/git/checkout to
return 500 instead of a usable state.
Agent Prompt
### Issue description
`readGitHeaderState()` assumes `HEAD` resolves to a commit and fails in an unborn repository (no commits). This turns valid repos into 500s for `/codex-api/git/branches` and for successful checkouts that still have no commits.

### Issue Context
`isMissingHeadError()` already exists and is used elsewhere to handle missing-HEAD cases; the header state path should do the same.

### Fix Focus Areas
- src/server/codexAppServerBridge.ts[2501-2525]
- src/server/codexAppServerBridge.ts[2439-2445]

### Implementation notes
- Wrap `rev-parse ... HEAD` and `git show ... HEAD` in `try/catch`.
- If the error matches `isMissingHeadError`, return `headSha/headSubject/headDate = null` (and keep `currentBranch` from `git branch --show-current`, plus `dirty` from `git status`).
- Only rethrow for non-missing-HEAD errors.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +6027 to +6084
if (req.method === 'GET' && url.pathname === '/codex-api/git/branch-commits') {
const rawCwd = (url.searchParams.get('cwd') ?? '').trim()
const branch = (url.searchParams.get('branch') ?? '').trim()
if (!rawCwd) {
setJson(res, 400, { error: 'Missing cwd' })
return
}
if (!branch) {
setJson(res, 400, { error: 'Missing branch' })
return
}
const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd)
try {
const gitRoot = await runCommandCapture('git', ['rev-parse', '--show-toplevel'], { cwd })
await runCommandCapture('git', ['rev-parse', '--verify', `${branch}^{commit}`], { cwd: gitRoot })
const output = await runCommandCapture(
'git',
['log', '-n', '12', '--date=short', '--format=%H%x09%h%x09%cd%x09%s', branch],
{ cwd: gitRoot },
)
const commits = output.split('\n').flatMap((line) => {
const [sha = '', shortSha = '', date = '', ...subjectParts] = line.split('\t')
const subject = subjectParts.join('\t').trim()
return sha.trim() && shortSha.trim()
? [{ sha: sha.trim(), shortSha: shortSha.trim(), date: date.trim(), subject: subject || shortSha.trim() }]
: []
})
setJson(res, 200, { data: commits })
} catch (error) {
setJson(res, 500, { error: getErrorMessage(error, 'Failed to load branch commits') })
}
return
}

if (req.method === 'POST' && url.pathname === '/codex-api/git/checkout-commit') {
const payload = await readJsonBody(req)
const record = asRecord(payload)
if (!record) {
setJson(res, 400, { error: 'Invalid body: expected object' })
return
}
const rawCwd = readNonEmptyString(record.cwd)
const sha = readNonEmptyString(record.sha)
if (!rawCwd) {
setJson(res, 400, { error: 'Missing cwd' })
return
}
if (!sha) {
setJson(res, 400, { error: 'Missing commit' })
return
}
const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd)
try {
const gitRoot = await runCommandCapture('git', ['rev-parse', '--show-toplevel'], { cwd })
await assertCleanGitWorktree(gitRoot)
const targetSha = await runCommandCapture('git', ['rev-parse', '--verify', `${sha}^{commit}`], { cwd: gitRoot })
await runCommand('git', ['checkout', '--detach', targetSha.trim()], { cwd: gitRoot })
setJson(res, 200, { data: await readGitHeaderState(gitRoot) })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Git option injection via refs 🐞 Bug ⛨ Security

The new /codex-api/git/branch-commits and /codex-api/git/checkout-commit endpoints pass
user-controlled branch/sha into git argv without validating that they are safe refs/hex SHAs;
values starting with '-' can be parsed as git options, leading to unintended command behavior.
Agent Prompt
### Issue description
`branch` and `sha` are user-controlled and are passed directly to git commands. Even without a shell, this enables *option injection* into git (args beginning with `-`) and can trigger unexpected behavior.

### Issue Context
This affects new endpoints:
- `GET /codex-api/git/branch-commits`
- `POST /codex-api/git/checkout-commit`

### Fix Focus Areas
- src/server/codexAppServerBridge.ts[6027-6058]
- src/server/codexAppServerBridge.ts[6061-6089]

### Implementation notes
- Reject `branch` values starting with `-` or containing whitespace/control chars.
- Prefer mapping `branch` to a *full ref* and verifying via `git show-ref --verify --quiet`:
  - Try `refs/heads/${branch}` and `refs/remotes/${branch}`; require exactly one to exist.
  - Use the verified full ref when running `git log` and `git rev-parse`.
- For `sha`, require a hex pattern (e.g. `/^[0-9a-fA-F]{7,40}$/`) before `rev-parse --verify`.
- Consider returning 400 for invalid inputs instead of 500.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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