Skip to content

fix(cmd/ssh): default ssh workdir to merged workspaceFolder#517

Merged
skevetter merged 4 commits intoskevetter:mainfrom
davzucky:516
Feb 25, 2026
Merged

fix(cmd/ssh): default ssh workdir to merged workspaceFolder#517
skevetter merged 4 commits intoskevetter:mainfrom
davzucky:516

Conversation

@davzucky
Copy link
Copy Markdown

@davzucky davzucky commented Feb 24, 2026

Summary

  • make devpod ssh prefer mergedConfig.WorkspaceFolder as the default workdir when --workdir is not provided
  • keep --workdir as the highest-priority override and preserve fallback to /workspaces/<workspace-id> when no merged workspace folder is available
  • add debug-level handling for workspace result load failures so SSH still connects using fallback behavior

Closes #516

Summary by CodeRabbit

  • Refactor
    • Improved workspace directory resolution for SSH sessions: prefers configured workspace folder when available, otherwise falls back to the workspace path.
    • Reworked SSH, GPG agent and forward-SSH invocation to build and quote commands safely, reducing shell injection risks.
    • Safer handling of flags and non-root execution so sessions run correctly when a non-root user is specified.
  • Bug Fixes
    • Minor logging tweaks around command composition.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Extracts workdir resolution into helpers and changes SSH invocation to build a shell-escaped argument slice (using shellescape), appends flags programmatically, and wraps with su -c for non-root users; workdir now prefers merged devcontainer workspaceFolder before falling back to /workspaces/<id>.

Changes

Cohort / File(s) Summary
SSH command & workdir
cmd/ssh.go
Adds resolveWorkdir and resolveMergedWorkspaceFolder helpers; replaces inline workdir logic with helpers; builds SSH command as an args slice using shellescape.Quote; appends flags via slice operations; updates GPG/SSH-forwarding command construction and wraps final command with su -c for non-root users; minor logging tweaks.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as Devpod CLI
  participant WS as WorkspaceClient
  participant Builder as Command Builder
  participant Shell as Remote Shell / SSH process
  participant GPG as GPG Agent Setup

  CLI->>WS: query merged workspace config
  WS-->>CLI: mergedConfig.WorkspaceFolder (or empty)
  CLI->>Builder: resolveWorkdir(flag, mergedConfig, workspaceID)
  Builder-->>Builder: construct args slice, append flags, quote parts
  Builder->>GPG: prepare GPG/SSH-forwarding commands (if needed)
  GPG-->>Builder: quoted GPG setup snippets
  Builder->>Shell: start SSH with composed, quoted command (wrap with su -c if non-root)
  Shell-->>CLI: SSH session established / output
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: fixing the default SSH working directory to use the merged workspace folder instead of a hardcoded path.
Linked Issues check ✅ Passed The PR implements all three priority levels from issue #516: respects --workdir flag, uses merged workspace folder when available, and falls back to /workspaces/ when needed.
Out of Scope Changes check ✅ Passed The changes to shellescape handling and command argument construction are implementation details necessary to safely construct the SSH commands with the new dynamic workdir resolution logic.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/ssh.go`:
- Around line 523-539: The code reads an attacker-controlled WorkspaceFolder
into SSHCmd.resolveWorkdir (result.MergedConfig.WorkspaceFolder) and later
interpolates that into a single-quoted shell command via fmt.Sprintf("'%s'
helper ssh-server ... --workdir '%s'", agent.ContainerDevPodHelperLocation,
workdir), allowing single-quote injection; fix by either rejecting/sanitizing
any workdir containing single quotes (and other shell metacharacters) before
returning from resolveWorkdir, or — preferably — stop building a shell string
and use an exec-style argument list / API that passes
agent.ContainerDevPodHelperLocation and the --workdir value as separate args
(avoiding a shell interpreter), updating the code paths that construct the
command to use the safe argument form instead of string interpolation.
- Around line 523-539: The resolveWorkdir function may call
provider.LoadWorkspaceResult with an empty workspaceConfig.Context causing
spurious debug logs; update SSHCmd.resolveWorkdir to first check
workspaceConfig.Context is non-empty before calling provider.LoadWorkspaceResult
(i.e., only call LoadWorkspaceResult(workspaceConfig.Context,
workspaceConfig.ID) when workspaceConfig.Context != ""), and otherwise skip that
lookup and proceed to the existing fallback logic that returns
filepath.Join("/workspaces", workspaceClient.Workspace()).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d0fa7d5 and 4e4c22e.

📒 Files selected for processing (1)
  • cmd/ssh.go

Comment thread cmd/ssh.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
cmd/ssh.go (1)

610-633: setupGPGAgent still uses the old unescaped string-join pattern — consider aligning with shellescape.

After this PR's fix, startTunnel consistently uses shellescape.QuoteCommand, but setupGPGAgent still:

  • Joins forwardAgent with a bare strings.Join(forwardAgent, " ") (Line 631), leaving any metacharacters in the helper-path or flag values unescaped.
  • Manually single-quotes gitKey via fmt.Sprintf("'%s'", gitKey) (Line 628). gitKey originates from git config user.signingKey, which is read from the cloned repo's git config and is therefore attacker-controlled. A value containing ' would break out of the single-quote context.
  • Wraps the su -c command with double-quote interpolation (Line 633) instead of the shellescape-based wrapping.
♻️ Proposed refactor to align with the new pattern
-	command := strings.Join(forwardAgent, " ")
+	command := shellescape.QuoteCommand(forwardAgent)
 	if cmd.User != "" && cmd.User != "root" {
-		command = fmt.Sprintf("su -c \"%s\" '%s'", command, cmd.User)
+		command = shellescape.QuoteCommand([]string{"su", "-c", command, cmd.User})
 	}

Also replace the manual quoting of gitKey:

 	if len(gitGpgKey) > 0 {
 		gitKey := strings.TrimSpace(string(gitGpgKey))
 		forwardAgent = append(forwardAgent, "--gitkey")
-		forwardAgent = append(forwardAgent, fmt.Sprintf("'%s'", gitKey))
+		forwardAgent = append(forwardAgent, gitKey)
 	}

(With the first change, shellescape.QuoteCommand will handle quoting gitKey correctly as part of forwardAgent.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/ssh.go` around lines 610 - 633, setupGPGAgent builds the shell command
insecurely by joining forwardAgent with strings.Join and manually quoting gitKey
and su -c; replace that with shellescape.QuoteCommand to safely quote each
argument. Specifically, stop doing fmt.Sprintf("'%s'", gitKey) and instead
append the raw gitKey (from gitGpgKey trimmed) to forwardAgent, then replace
command := strings.Join(forwardAgent, " ") and the su -c wrapping with command
:= shellescape.QuoteCommand(forwardAgent) (and if you must run as non-root wrap
with su -c using shellescape.QuoteArgument or equivalent to quote the entire
command safely); follow the same pattern used in startTunnel and ensure the
debug flag logic and ownerTrust/socket args remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cmd/ssh.go`:
- Around line 610-633: setupGPGAgent builds the shell command insecurely by
joining forwardAgent with strings.Join and manually quoting gitKey and su -c;
replace that with shellescape.QuoteCommand to safely quote each argument.
Specifically, stop doing fmt.Sprintf("'%s'", gitKey) and instead append the raw
gitKey (from gitGpgKey trimmed) to forwardAgent, then replace command :=
strings.Join(forwardAgent, " ") and the su -c wrapping with command :=
shellescape.QuoteCommand(forwardAgent) (and if you must run as non-root wrap
with su -c using shellescape.QuoteArgument or equivalent to quote the entire
command safely); follow the same pattern used in startTunnel and ensure the
debug flag logic and ownerTrust/socket args remain unchanged.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4e4c22e and ddb024e.

📒 Files selected for processing (1)
  • cmd/ssh.go

Comment thread cmd/ssh.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a 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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/ssh.go`:
- Line 542: Swap the OS-specific filepath.Join call to POSIX path.Join for the
container workdir fallback: replace the return expression that currently uses
filepath.Join("/workspaces", workspaceClient.Workspace()) with
path.Join("/workspaces", workspaceClient.Workspace()) and update imports to
include the standard "path" package (and remove or keep filepath only if still
needed elsewhere) so the container --workdir always uses forward slashes.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ddb024e and db578e1.

📒 Files selected for processing (1)
  • cmd/ssh.go

Comment thread cmd/ssh.go Outdated
@skevetter skevetter removed the size/s label Feb 25, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
cmd/ssh.go (1)

533-543: LGTM — workdir resolution priority chain is correct.

--workdir flag → mergedWorkspaceFolderpath.Join("/workspaces", ...) fallback matches the stated priority order from the issue, and using path.Join (POSIX) instead of filepath.Join ensures forward slashes on Windows hosts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/ssh.go` around lines 533 - 543, Summary: The workdir resolution in
function resolveWorkdir is correct and follows the intended priority chain
(--workdir, resolveMergedWorkspaceFolder, fallback to path.Join("/workspaces",
workspaceClient.Workspace())). No code changes required; keep resolveWorkdir and
its use of resolveMergedWorkspaceFolder and path.Join as-is to preserve
POSIX-style slashes on Windows and the documented priority order.
🧹 Nitpick comments (1)
cmd/ssh.go (1)

174-250: resolveWorkdir is not applied in the tailscale/DaemonClient path.

jumpContainerTailscale calls machine.RunSSHSession directly and never invokes startTunnel, so the new workdir resolution (--workdir flag, mergedWorkspaceFolder, fallback) has no effect for Tailscale-backed workspaces. If users of that path rely on the devcontainer workspaceFolder, they won't benefit from this fix.

This is pre-existing behaviour and out of scope for this PR, but worth a follow-up issue to bring the tailscale path to parity.

Would you like me to open a follow-up issue to track workspaceFolder resolution for the tailscale/DaemonClient path?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/ssh.go` around lines 174 - 250, jumpContainerTailscale bypasses
resolveWorkdir because it calls machine.RunSSHSession/DirectTunnel directly
instead of using the existing startTunnel flow that applies workdir resolution;
update jumpContainerTailscale to resolve the workspace before starting the
session by reusing the same resolveWorkdir/startTunnel logic (or call
startTunnel) used for non-Tailscale paths so the --workdir flag and
mergedWorkspaceFolder fallback are honored; locate references to
jumpContainerTailscale, startTunnel, resolveWorkdir, client.DirectTunnel, and
machine.RunSSHSession to implement the change and ensure the resolved workdir is
injected into the SSH session options or tunnel startup flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@cmd/ssh.go`:
- Around line 533-543: Summary: The workdir resolution in function
resolveWorkdir is correct and follows the intended priority chain (--workdir,
resolveMergedWorkspaceFolder, fallback to path.Join("/workspaces",
workspaceClient.Workspace())). No code changes required; keep resolveWorkdir and
its use of resolveMergedWorkspaceFolder and path.Join as-is to preserve
POSIX-style slashes on Windows and the documented priority order.

---

Nitpick comments:
In `@cmd/ssh.go`:
- Around line 174-250: jumpContainerTailscale bypasses resolveWorkdir because it
calls machine.RunSSHSession/DirectTunnel directly instead of using the existing
startTunnel flow that applies workdir resolution; update jumpContainerTailscale
to resolve the workspace before starting the session by reusing the same
resolveWorkdir/startTunnel logic (or call startTunnel) used for non-Tailscale
paths so the --workdir flag and mergedWorkspaceFolder fallback are honored;
locate references to jumpContainerTailscale, startTunnel, resolveWorkdir,
client.DirectTunnel, and machine.RunSSHSession to implement the change and
ensure the resolved workdir is injected into the SSH session options or tunnel
startup flow.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between db578e1 and 30d53c0.

📒 Files selected for processing (1)
  • cmd/ssh.go

@skevetter skevetter merged commit 288c56d into skevetter:main Feb 25, 2026
40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

devpod ssh should default to devcontainer workspaceFolder

2 participants