-
Notifications
You must be signed in to change notification settings - Fork 780
fix: use git root for session tracking to prevent session loss #339
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix: use git root for session tracking to prevent session loss #339
Conversation
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. WalkthroughChat, shell, and Git handlers now resolve the repository root via Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client
participant Server as Server
participant GitCLI as Git CLI
participant SessionStore as Session Store
participant PTY as PTY Process
Client->>Server: Request (chat / shell / git) with projectPath, sessionId
Server->>GitCLI: git rev-parse --show-toplevel (cwd=projectPath)
GitCLI-->>Server: gitRoot or error
alt gitRoot returned
Server->>SessionStore: sanitize sessionId, derive pty/session key using gitRoot
Server->>GitCLI: run requested Git command (cwd=gitRoot)
GitCLI-->>Server: command result
alt shell resume flow
Server->>PTY: attempt resume from resumePath (gitRoot or fallback)
PTY-->>Server: resumed or failed
alt resumed
Server->>Client: replay buffered output
Server->>Client: send CSI 999999 H (scroll-to-bottom)
else failed
Server->>PTY: start new shell (cwd fallback)
end
end
Server-->>Client: Response contextualized to gitRoot
else error
Server-->>Client: Validation/fallback error
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
server/routes/git.js (2)
61-76: FixvalidateGitRepository:gitRootis used before assignment.
{ cwd: gitRoot }throws aReferenceError, so validation always fails. UseprojectPathas the cwd, then normalize the returned root.🐛 Proposed fix
- const { stdout: gitRoot } = await execAsync('git rev-parse --show-toplevel', { cwd: gitRoot }); - const normalizedGitRoot = path.resolve(gitRoot.trim()); + const { stdout } = await execAsync('git rev-parse --show-toplevel', { cwd: projectPath }); + const normalizedGitRoot = path.resolve(stdout.trim());
406-411: DefinegitRootbefore use in these routes.
checkout,create-branch,commits,commit-diff, andgenerate-commit-messagecallexecAsync(..., { cwd: gitRoot })without defininggitRoot, which will crash at runtime. Addconst gitRoot = await validateGitRepository(projectPath);in each handler.🐛 Example fix (apply similarly to the other handlers)
try { const projectPath = await getActualProjectPath(project); + const gitRoot = await validateGitRepository(projectPath); // Checkout the branch const { stdout } = await execAsync(`git checkout "${branch}"`, { cwd: gitRoot });Also applies to: 427-432, 448-477, 499-506, 528-538
🤖 Fix all issues with AI agents
In `@server/index.js`:
- Around line 49-67: The getGitRoot function uses execSync with string
interpolation which allows shell injection via projectPath; replace this with
execFileSync (or spawnSync) calling git directly with an args array (e.g.,
['-C', projectPath, 'rev-parse', '--show-toplevel']) and { encoding: 'utf8' } to
avoid shell interpretation, preserve the try/catch behavior, trim and return
gitRoot when present, and keep the console.log debug message; update references
to execSync to execFileSync and ensure projectPath is validated (non-empty)
before use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
server/routes/git.js (1)
170-191: UsegitRootfor filesystem paths that originate from git commands.
filevalues come from git commands executed atgitRoot, making them relative to the git root. Joining them withprojectPath(which may be a subdirectory) breaks path resolution when the project directory is not the repository root. Switch all filesystem operations at the mentioned locations to usegitRootinstead ofprojectPath.Affected locations
- Lines 170–191: untracked file diff
- Lines 249–258: tracked file content read
- Lines 557–563: generate-commit-message file read
- Lines 1069–1076: discard-untracked delete
- Lines 1118–1127: delete-untracked delete
🤖 Fix all issues with AI agents
In `@server/routes/git.js`:
- Around line 341-348: The git command usage in the staging/commit and branch
operations uses execAsync with interpolated strings (see the loop over files
using `file`, the `git commit` using `message`, and branch operations using
`branch`) which permits command injection; replace these calls to
shell-interpolated exec with a safe execFile/spawn variant that accepts an
argument array (e.g., create or use an `execFileAsync` wrapper and call
execFileAsync('git', ['add', file'], {cwd: gitRoot}) for adds,
execFileAsync('git', ['commit', '-m', message], {cwd: gitRoot}) for commits, and
execFileAsync('git', ['checkout', '-b', branch], ...) or similar for branch
operations), ensuring you stop building command strings and pass user-controlled
variables as separate arguments to the execFile call (update all uses of
`execAsync` that pass `file`, `message`, or `branch`).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@server/index.js`:
- Around line 1063-1075: The shellCommand construction interpolates unsanitized
sessionId into shell strings (see variables shellCommand, resumePath,
hasSession, sessionId, projectPath, shellGitRoot) causing command-injection
risk; create a sanitized safeSessionId (use the same safe pattern used elsewhere
in the repo to either validate/whitelist or properly escape session IDs) and
replace all direct uses of sessionId in the commands that build shellCommand
(both Windows PowerShell branch and non-Windows branch, including the cursor
provider command sites referenced) with safeSessionId so only safe characters
are allowed/escaped before embedding into the `claude --resume ...` invocation.
🧹 Nitpick comments (1)
server/index.js (1)
49-72: Function defined before its dependency is imported.
getGitRootusesexecFileSync(line 59), but the import statement forexecFileSyncis at line 80, after the function definition. In ES modules, imports are hoisted, so this will work correctly. However, this ordering is unconventional and may confuse readers.Consider moving the function definition after the imports for better code organization and clarity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@server/index.js`:
- Around line 952-955: The sanitized sessionId handling currently allows an
empty string to pass through, which can cause session collisions; update the
logic around rawSessionId/sessionId (and where hasSession is used) to explicitly
reject requests when rawSessionId was provided but
String(rawSessionId).replace(/[^a-zA-Z0-9._-]/g, '') yields an empty
string—e.g., send a validation error/HTTP 400 or close the connection and return
early instead of falling back to a default session key; ensure the check uses
the same sanitization expression so you only reject when the original input
existed but sanitization removed all characters.
🧹 Nitpick comments (1)
server/index.js (1)
49-72: Add logging when Git is missing to aid deployment debugging.Lines 56-72 silently fall back when git isn't available, which can mask deployment issues where the git binary is absent. The function will return
projectPathinstead of the git root, causing sessions to be keyed differently than intended. Add a warning for ENOENT to make this obvious.♻️ Suggested improvement
function getGitRoot(projectPath) { if (!projectPath) return projectPath; try { const gitRoot = execFileSync( 'git', ['-C', projectPath, 'rev-parse', '--show-toplevel'], { encoding: 'utf8' } ).trim(); if (gitRoot) { console.log('🔧 Git root detected:', gitRoot, 'from:', projectPath); return gitRoot; } } catch (e) { - // Not a git repository, use the original path + // Not a git repository, use the original path + if (e?.code === 'ENOENT') { + console.warn('[WARN] git not found; falling back to projectPath for session tracking'); + } } return projectPath; }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
server/routes/git.js (1)
446-460:⚠️ Potential issue | 🔴 CriticalUnvalidated
limitparameter enables command injection.The
limitquery parameter is interpolated directly into the shell command without validation. An attacker can inject shell commands:GET /commits?project=foo&limit=10;rm%20-rf%20/🔒 Recommended fix
- const { project, limit = 10 } = req.query; + const { project, limit: limitParam = '10' } = req.query; + const limit = Math.min(Math.max(1, parseInt(limitParam, 10) || 10), 100); ... - const { stdout } = await execAsync( - `git log --pretty=format:'%H|%an|%ae|%ad|%s' --date=relative -n ${limit}`, + const { stdout } = await execFileAsync( + 'git', + ['log', '--pretty=format:%H|%an|%ae|%ad|%s', '--date=relative', '-n', String(limit)], { cwd: gitRoot } );
🤖 Fix all issues with AI agents
In `@server/routes/git.js`:
- Around line 180-182: The code joins user-supplied file with gitRoot into
filePath without validation, allowing path traversal; fix by normalizing and
resolving the resulting path and rejecting any paths that escape gitRoot (e.g.,
compute resolved = path.resolve(gitRoot, file) or use path.relative and ensure
the result does not start with '..' or escape separators), then only proceed
with fs.stat / read / write using the validated filePath; apply this check to
all places that build filePath (references: the filePath variable and gitRoot
usage in server/routes/git.js, including the other occurrences you noted) and
return a 400/403 error when validation fails.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
server/routes/git.js (1)
1-12:⚠️ Potential issue | 🔴 CriticalSummary: Security foundations are good, but command injection fixes are incomplete.
The PR adds solid security infrastructure:
- ✅
validateFilePathfor path traversal protection- ✅
execFileAsyncfor safe command execution- ✅ Limit parameter sanitization in
/commits- ✅ Git root resolution for session persistence
However,
execFileAsyncis only used forgit log. All other git commands (~15+ locations) still useexecAsyncwith string interpolation, leaving command injection vulnerabilities. The past review marked these as "Addressed," but the current code shows they are not.Consider a systematic pass to convert all
execAsynccalls that include user input (file,message,branch,commit) to useexecFileAsyncwith argument arrays.
🤖 Fix all issues with AI agents
In `@server/routes/git.js`:
- Line 1089: The discard endpoint is vulnerable because the user-controlled file
variable is directly interpolated into shell commands passed to execAsync (see
execAsync calls using `git status --porcelain "${file}"`, and the similar git
commands at the other two sites), so replace these shell-interpolated calls with
a safe child_process invocation that passes the file as a distinct argument
(e.g., use execFile/spawn or execAsync with an args array) or explicitly
sanitize/escape the file name (reject inputs with shell metacharacters or
path-traversal) before calling git; ensure validation logic around `file` and
`gitRoot` (the existing path checks) is reused and applied to all three
execAsync invocations in the discard endpoint to avoid command injection.
- Line 258: The git file-with-diff handler is vulnerable because user-controlled
file is interpolated into execAsync commands; replace those execAsync calls with
execFileAsync (or spawn/child_process.execFile) and pass the git command and
file as separate args instead of a shell-interpolated string—specifically update
the calls that produce statusOutput (currently using execAsync(`git status
--porcelain "${file}"`, { cwd: gitRoot })), the call that produces the diff
output, and the call that produces blame/other outputs to use
execFileAsync('git', ['status','--porcelain', file], { cwd: gitRoot }) (and
analogous arg arrays for diff/blame) so the file parameter is not interpreted by
the shell; ensure error handling and cwd usage remain the same.
- Line 194: The git commands interpolate the untrusted req.query.file into shell
strings via execAsync (e.g., the call that runs `git status --porcelain
"${file}"` and similar calls later), which allows shell injection; change these
to invoke git without a shell by using a spawn/execFile-style API that accepts
args arrays (e.g., replace execAsync with a promisified child_process.execFile
or child_process.spawn with args: ['status','--porcelain', file], and similarly
for the other git invocations referenced in the diff), keep using
validateFilePath for filesystem checks but stop string-interpolating file into
shell commands so the file value is passed as an argument not parsed by a shell.
- Line 436: The checkout/create-branch code is vulnerable because branch is
interpolated into a shell string passed to execAsync; replace this with a safe
execFile/child_process spawn call that passes the command and branch as separate
arguments (e.g., call execFile('git', ['checkout', branch], { cwd: gitRoot })
via the same promisified helper) and/or validate/sanitize the branch parameter
before use (allow only an explicit safe regex like alphanumerics, dot, dash,
slash and reject names containing whitespace, shell metacharacters, or '..' and
leading '-' ). Update both occurrences that call execAsync with inline templates
(the one using `git checkout "${branch}"` and the other at the later occurrence)
to use the argument-based exec and add the branch validation logic where the
request body branch is read.
- Line 1040: The publish endpoint is vulnerable because the branch value from
req.body is interpolated into a shell string passed to execAsync (see execAsync
call using `git push --set-upstream ${remoteName} ${branch}`); fix by
validating/sanitizing inputs (ensure branch and remoteName match a strict
allowlist/regex like /^[A-Za-z0-9._/-]+$/) and avoid shell interpolation by
calling Git with a non-shell exec variant (use
child_process.execFile/execFileSync or spawn with args: e.g., run git with args
['push','--set-upstream', remoteName, branch'] and cwd set to gitRoot) so
user-controlled branch cannot inject commands. Ensure both branch and remoteName
are validated before calling execFile/spawn.
- Around line 568-570: The git diff invocation interpolates the user-controlled
variable file into a shell string, enabling command injection; replace the
string-based execAsync call with a safe execFile/spawn variant that passes
arguments separately (e.g., call execFileAsync or spawn('git',
['diff','HEAD','--', file], {cwd: gitRoot}) or use a library like execa to pass
args) so the file name is never interpreted by a shell, and keep/ensure
validateFilePath is applied before calling execFile/spawn; update the call site
where execAsync is used for the generate-commit-message flow (the execAsync call
around `git diff HEAD -- "${file}"`) to use this arguments-based API.
🧹 Nitpick comments (1)
server/routes/git.js (1)
505-507: UseexecFileAsyncfor consistency in commit stats lookup.While
commit.hashcomes from git output rather than direct user input, usingexecFileAsynchere maintains consistency with the security pattern used for the main log command above.♻️ Suggested refactor
try { - const { stdout: stats } = await execAsync( - `git show --stat --format='' ${commit.hash}`, - { cwd: gitRoot } - ); + const { stdout: stats } = await execFileAsync( + 'git', + ['show', '--stat', '--format=', commit.hash], + { cwd: gitRoot } + ); commit.stats = stats.trim().split('\n').pop(); // Get the summary line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
server/routes/git.js (1)
194-224:⚠️ Potential issue | 🟠 MajorAdd
--before file pathspecs to prevent option injection.Several git commands pass user-supplied
fileas a pathspec without--. A filename starting with-will be interpreted as an option (e.g.,--all), potentially altering behavior (stage/restore more files than intended). Add--forstatus(lines 194, 258, 1093, 1141),add(line 368),restore(line 1115), andreset(line 1118).🛠️ Suggested fix pattern
- const { stdout: statusOutput } = await execFileAsync('git', ['status', '--porcelain', file], { cwd: gitRoot }); + const { stdout: statusOutput } = await execFileAsync('git', ['status', '--porcelain', '--', file], { cwd: gitRoot });Apply similarly to
add,restore, andresetcommands.
🤖 Fix all issues with AI agents
In `@server/routes/git.js`:
- Around line 22-31: The current validateFilePath function fails when the
repository root is filesystem root because resolvedRoot + path.sep can produce
'//' and reject valid paths; replace the startsWith check with a containment
check using path.relative: compute const rel = path.relative(resolvedRoot,
resolvedPath) and throw an Error if rel === ''? no — instead throw if
rel.startsWith('..') or path.isAbsolute(rel); otherwise return resolvedPath.
Update the logic inside validateFilePath to use this path.relative-based
containment check to correctly handle root-path and other edge cases.
When Claude changes directories during work (e.g., cd into subdirectory), sessions were being lost because: 1. Chat sessions used the current working directory (cwd) as identifier 2. Terminal sessions used projectPath for session key 3. Source Control operations failed in subdirectories This fix detects the git repository root and uses it consistently: - Chat tab: Uses git root for cwd option in Claude SDK calls - Terminal tab: Uses git root for PTY session key and resume commands - Source Control tab: validateGitRepository now returns git root instead of throwing error, allowing all git operations from subdirectories Fixes session loss when working in monorepos or when Claude navigates to different directories within a project.
- Fix validateGitRepository to use projectPath instead of undefined gitRoot - Add missing gitRoot definition to checkout, create-branch, commits, commit-diff, and generate-commit-message routes - Rename stdout destructuring to avoid variable shadowing
Use execFileSync with argument array instead of execSync with string interpolation to prevent potential command injection via projectPath.
- Add sessionId sanitization to prevent command injection - Scroll terminal to bottom after buffer replay on reconnect
- Reject and close connection if sessionId contains only invalid chars - Add warning log when git binary is not found (ENOENT)
File paths returned by git commands (status --porcelain, etc.) are relative to the git repository root, not the current projectPath. When Claude changes directories during work, projectPath may be a subdirectory, causing file operations to fail. This fixes file read/write/delete operations in: - /diff endpoint (untracked file content) - /file-with-diff endpoint (current file content) - /generate-commit-message endpoint (untracked file content) - /discard endpoint (delete untracked files) - /delete-untracked endpoint (delete untracked files)
- Add validateFilePath() helper to prevent path traversal attacks by ensuring file paths stay within the git repository root - Fix /commits endpoint: validate and sanitize limit parameter, use execFileAsync instead of string interpolation to prevent command injection (e.g., limit=10;rm -rf /) - Apply path validation to all endpoints that read/write files: /diff, /file-with-diff, /generate-commit-message, /discard, /delete-untracked Security issues identified by CodeRabbit review.
…ection prevention Convert all git command executions from shell-interpolated execAsync to argument-based execFileAsync to prevent command injection attacks. Affected endpoints: - /diff: git status, git show, git diff - /file-with-diff: git status, git show - /commit: git add, git commit - /checkout: git checkout - /create-branch: git checkout -b - /commits: git show --stat - /commit-diff: git show - /generate-commit-message: git diff - /remote-status: git rev-parse, git remote, git rev-list - /fetch: git rev-parse, git fetch - /pull: git rev-parse, git pull - /push: git rev-parse, git push - /publish: git rev-parse, git remote, git push - /discard: git status, git restore, git reset - /delete-untracked: git status Using execFileAsync with argument arrays prevents shell interpretation of user-controlled values like file paths, branch names, and commit messages.
- Add '--' before file pathspecs in git commands to prevent option injection (e.g., filename starting with '-' being interpreted as option) Affected commands: status, add, restore, reset - Improve validateFilePath to use path.relative() for robust containment check that correctly handles edge cases like filesystem root as repo root
805df58 to
c7b80c2
Compare
Summary
When Claude changes directories during work (e.g.,
cdinto a subdirectory), sessions were being lost because the UI tracked sessions by the current working directory rather than the git repository root.This fix detects the git repository root and uses it consistently across all components:
cwdoption in Claude SDK callsvalidateGitRepositorynow returns git root instead of throwing an error, allowing all git operations from subdirectoriesProblem
/home/user/myproject/home/user/myproject/src/componentsduring workSolution
Added
getGitRoot()helper function that detects the git repository root usinggit rev-parse --show-toplevel. This root is then used for:Test plan
Files changed
server/index.js- AddedgetGitRoot()helper, patched chat and terminal handlersserver/routes/git.js- ModifiedvalidateGitRepository()to return git rootSummary by CodeRabbit
New Features
Bug Fixes
Improvements
✏️ Tip: You can customize this high-level summary in your review settings.