diff --git a/cli/configssh.go b/cli/configssh.go index 7676e82c4a7cb..bb1025fa0c9dc 100644 --- a/cli/configssh.go +++ b/cli/configssh.go @@ -291,7 +291,7 @@ func (r *RootCmd) configSSH() *serpent.Command { } } root := r.createConfig() - homedir, err := os.UserHomeDir() + homedir, err := realHomeDir() if err != nil { return xerrors.Errorf("user home dir failed: %w", err) } @@ -880,6 +880,20 @@ func currentBinPath(w io.Writer) (string, error) { return exePath, nil } +// realHomeDir returns the user's actual home directory. +// In snap environments, os.UserHomeDir() returns the snap's isolated home +// directory (e.g., ~/snap/coder/x3/), but we need the actual user's home +// directory for SSH config. This function checks for SNAP_REAL_HOME first, +// which contains the actual home directory in snap environments. +func realHomeDir() (string, error) { + // In snap environments, SNAP_REAL_HOME contains the actual user home. + if snapHome := os.Getenv("SNAP_REAL_HOME"); snapHome != "" { + return snapHome, nil + } + // Fall back to the standard method for non-snap environments. + return os.UserHomeDir() +} + // diffBytes takes two byte slices and diffs them as if they were in a // file named name. // nolint: revive // Color is an option, not a control coupling. diff --git a/cli/configssh_test.go b/cli/configssh_test.go index 7e42bfe81a799..75ab451f1acc6 100644 --- a/cli/configssh_test.go +++ b/cli/configssh_test.go @@ -753,3 +753,54 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) { }) } } + +func TestConfigSSH_SnapEnvironment(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Snap is not available on Windows") + } + + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + + // Create a temporary directory that simulates the real home + realHome := t.TempDir() + realSSHDir := filepath.Join(realHome, ".ssh") + err := os.MkdirAll(realSSHDir, 0o700) + require.NoError(t, err) + realSSHConfig := filepath.Join(realSSHDir, "config") + + // Create a separate directory that simulates the snap home + snapHome := t.TempDir() + + // Set SNAP_REAL_HOME to point to the real home directory + t.Setenv("SNAP_REAL_HOME", realHome) + // Set HOME to the snap directory to simulate snap environment + t.Setenv("HOME", snapHome) + + // Run config-ssh with default path (~/.ssh/config) + // It should use SNAP_REAL_HOME and write to realSSHConfig + args := []string{ + "config-ssh", + "--yes", // Skip confirmation prompts + } + inv, root := clitest.New(t, args...) + clitest.SetupConfig(t, client, root) + + err = inv.Run() + require.NoError(t, err, "config-ssh should succeed in snap environment") + + // Verify that the config file was written to the REAL home directory, + // not the snap home directory + _, err = os.Stat(realSSHConfig) + require.NoError(t, err, "config file should exist in real home directory") + + // Verify the config file contains the expected coder section + content, err := os.ReadFile(realSSHConfig) + require.NoError(t, err) + require.Contains(t, string(content), "# ------------START-CODER-----------", "config should contain coder section") + + // Verify that nothing was written to the snap home directory + snapSSHConfig := filepath.Join(snapHome, ".ssh", "config") + _, err = os.Stat(snapSSHConfig) + require.True(t, os.IsNotExist(err), "config file should NOT exist in snap home directory") +}