From d85e6003e8068057cb49c1d32ca0401ccfeed791 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 9 Sep 2026 16:17:06 -0700 Subject: [PATCH] Follow a symlinked temporary directory to the TUI socket MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macOS's /tmp is a symlink to /private/tmp, and it is what os.TempDir answers whenever TMPDIR is unset — an ssh session, a launchd job, a shell started outside the login environment. The runtime-directory fallback refused any symlink there, so hey tui failed with "temporary directory is not a directory" while every other command worked. Resolve the link and inspect the directory it names; the per-user directory under it is still created and validated as before. --- internal/tui/open_remote_unix.go | 7 +++++-- internal/tui/open_remote_unix_test.go | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/internal/tui/open_remote_unix.go b/internal/tui/open_remote_unix.go index 45f0e586..88363c44 100644 --- a/internal/tui/open_remote_unix.go +++ b/internal/tui/open_remote_unix.go @@ -76,12 +76,15 @@ func tuiRuntimeDir() (string, error) { return ensurePrivateDirectory(filepath.Join(runtimeDir, tuiRuntimeDirname)) } - tempDir := os.TempDir() + tempDir, err := filepath.EvalSymlinks(os.TempDir()) + if err != nil { + return "", fmt.Errorf("inspect temporary directory: %w", err) + } info, err := os.Lstat(tempDir) if err != nil { return "", fmt.Errorf("inspect temporary directory: %w", err) } - if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { + if !info.IsDir() { return "", fmt.Errorf("temporary directory is not a directory") } if info.Mode().Perm()&0o022 != 0 && info.Mode()&os.ModeSticky == 0 { diff --git a/internal/tui/open_remote_unix_test.go b/internal/tui/open_remote_unix_test.go index 04489195..b3cdf306 100644 --- a/internal/tui/open_remote_unix_test.go +++ b/internal/tui/open_remote_unix_test.go @@ -80,6 +80,28 @@ func TestTopicRemoteFallbackUsesPrivatePerUserDirectory(t *testing.T) { } } +// macOS's /tmp is a symlink to /private/tmp, and it is what os.TempDir answers whenever +// TMPDIR is unset — an ssh session, a launchd job, a shell started without the login +// environment — so the fallback has to follow the link rather than refuse it. +func TestTopicRemoteFallbackFollowsSymlinkedTemporaryDirectory(t *testing.T) { + t.Setenv("XDG_RUNTIME_DIR", "") + target := t.TempDir() + link := filepath.Join(t.TempDir(), "tmp") + if err := os.Symlink(target, link); err != nil { + t.Fatalf("symlink temporary directory: %v", err) + } + t.Setenv("TMPDIR", link) + + path := mustTUISocketPath(t, "omarchy") + resolved, err := filepath.EvalSymlinks(target) + if err != nil { + t.Fatalf("resolve temporary directory: %v", err) + } + if got := filepath.Dir(filepath.Dir(path)); got != resolved { + t.Fatalf("socket %s is not under the resolved temporary directory %s", path, resolved) + } +} + func TestTopicRemoteRejectsInsecureRuntimeDirectory(t *testing.T) { runtimeDir := t.TempDir() if err := os.Chmod(runtimeDir, 0o755); err != nil {