Skip to content

Commit 10cc1eb

Browse files
gi11esclaude
andauthored
fix: use dedicated tmux socket to preserve TCC permissions across restarts (#42)
* fix: use dedicated tmux socket and kill server on quit to preserve TCC permissions The tmux server survived Deckard restarts but retained stale TCC permissions from the old Deckard process. macOS revokes TCC access when the responsible app exits, causing "Operation not permitted" errors in shells for protected directories like ~/Documents. Fix: use a dedicated `-L deckard` socket (isolating from user tmux) and kill the server on quit + launch so a fresh server with valid TCC permissions is created each time. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: keep tmux sessions alive across restarts, only isolate socket The original fix killed the tmux server on quit, which lost all terminal content — defeating the purpose of tmux persistence. The TCC issue was likely a transient fluke (sleep/wake corruption), not a systemic problem. Keep the dedicated `-L deckard` socket for isolation from user tmux, but restore session persistence: detach() on quit, orphan cleanup on launch, and reconnect via -A on restore. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
1 parent 2a48e5b commit 10cc1eb

File tree

1 file changed

+11
-8
lines changed

1 file changed

+11
-8
lines changed

Sources/Terminal/TerminalSurface.swift

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ class TerminalSurface: NSObject, LocalProcessTerminalViewDelegate {
6666
/// The tmux session name, if this terminal is wrapped in tmux.
6767
var tmuxSessionName: String?
6868

69+
/// Dedicated tmux socket so Deckard's server is isolated from the user's.
70+
/// A fresh server (with valid TCC permissions) is created on each launch.
71+
static let tmuxSocket = "deckard"
72+
6973
private let terminalView: DeckardTerminalView
7074
private var processExited = false
7175
private var pendingInitialInput: String?
@@ -125,7 +129,7 @@ class TerminalSurface: NSObject, LocalProcessTerminalViewDelegate {
125129
DispatchQueue.global(qos: .userInteractive).async {
126130
let task = Process()
127131
task.executableURL = URL(fileURLWithPath: path)
128-
task.arguments = ["send-keys", "-t", name, "-X", "cancel"]
132+
task.arguments = ["-L", Self.tmuxSocket, "send-keys", "-t", name, "-X", "cancel"]
129133
task.standardOutput = FileHandle.nullDevice
130134
task.standardError = FileHandle.nullDevice
131135
try? task.run()
@@ -170,7 +174,7 @@ class TerminalSurface: NSObject, LocalProcessTerminalViewDelegate {
170174
// tmux new-session -A: attach if exists, create if not
171175
// -s: session name, -c: starting directory (only for new sessions)
172176
// -u: force UTF-8 mode for proper emoji/wide character handling
173-
var args = ["-u", "new-session", "-A", "-s", sessionName]
177+
var args = ["-L", Self.tmuxSocket, "-u", "new-session", "-A", "-s", sessionName]
174178
if let cwd = workingDirectory { args += ["-c", cwd] }
175179

176180
terminalView.startProcess(
@@ -239,7 +243,6 @@ class TerminalSurface: NSObject, LocalProcessTerminalViewDelegate {
239243
guard !processExited else { return }
240244
processExited = true
241245
terminalView.process?.terminate()
242-
// Kill the tmux session when tab is explicitly closed
243246
killTmuxSession()
244247
}
245248

@@ -271,7 +274,7 @@ class TerminalSurface: NSObject, LocalProcessTerminalViewDelegate {
271274
guard let name = tmuxSessionName, let path = Self.tmuxPath else { return }
272275
let task = Process()
273276
task.executableURL = URL(fileURLWithPath: path)
274-
task.arguments = ["kill-session", "-t", name]
277+
task.arguments = ["-L", Self.tmuxSocket, "kill-session", "-t", name]
275278
task.standardOutput = FileHandle.nullDevice
276279
task.standardError = FileHandle.nullDevice
277280
try? task.run()
@@ -313,7 +316,7 @@ class TerminalSurface: NSObject, LocalProcessTerminalViewDelegate {
313316
}
314317
let task = Process()
315318
task.executableURL = URL(fileURLWithPath: tmuxPath)
316-
task.arguments = args
319+
task.arguments = ["-L", tmuxSocket] + args
317320
task.standardOutput = FileHandle.nullDevice
318321
task.standardError = FileHandle.nullDevice
319322
try? task.run()
@@ -326,7 +329,7 @@ class TerminalSurface: NSObject, LocalProcessTerminalViewDelegate {
326329
guard let path = tmuxPath else { return nil }
327330
let task = Process()
328331
task.executableURL = URL(fileURLWithPath: path)
329-
task.arguments = ["list-panes", "-t", sessionName, "-F", "#{pane_pid}"]
332+
task.arguments = ["-L", tmuxSocket, "list-panes", "-t", sessionName, "-F", "#{pane_pid}"]
330333
let pipe = Pipe()
331334
task.standardOutput = pipe
332335
task.standardError = FileHandle.nullDevice
@@ -344,7 +347,7 @@ class TerminalSurface: NSObject, LocalProcessTerminalViewDelegate {
344347
guard let path = tmuxPath else { return }
345348
let task = Process()
346349
task.executableURL = URL(fileURLWithPath: path)
347-
task.arguments = ["list-sessions", "-F", "#{session_name}"]
350+
task.arguments = ["-L", tmuxSocket, "list-sessions", "-F", "#{session_name}"]
348351
let pipe = Pipe()
349352
task.standardOutput = pipe
350353
task.standardError = FileHandle.nullDevice
@@ -357,7 +360,7 @@ class TerminalSurface: NSObject, LocalProcessTerminalViewDelegate {
357360
if name.hasPrefix("deckard-") && !activeSessions.contains(name) {
358361
let kill = Process()
359362
kill.executableURL = URL(fileURLWithPath: path)
360-
kill.arguments = ["kill-session", "-t", name]
363+
kill.arguments = ["-L", tmuxSocket, "kill-session", "-t", name]
361364
kill.standardOutput = FileHandle.nullDevice
362365
kill.standardError = FileHandle.nullDevice
363366
try? kill.run()

0 commit comments

Comments
 (0)