diff --git a/internal/cmd/up.go b/internal/cmd/up.go index 4648bf51..c7912f9f 100644 --- a/internal/cmd/up.go +++ b/internal/cmd/up.go @@ -281,6 +281,10 @@ func ensureSession(t *tmux.Tmux, sessionName, workDir, role string) error { if err := t.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil { // Non-fatal } + + // Accept bypass permissions warning dialog if it appears. + _ = t.AcceptBypassPermissionsWarning(sessionName) + time.Sleep(constants.ShutdownNotifyDelay) // Inject startup nudge for predecessor discovery via /resume @@ -330,6 +334,10 @@ func ensureWitness(t *tmux.Tmux, sessionName, rigPath, rigName string) error { if err := t.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil { // Non-fatal } + + // Accept bypass permissions warning dialog if it appears. + _ = t.AcceptBypassPermissionsWarning(sessionName) + time.Sleep(constants.ShutdownNotifyDelay) // Inject startup nudge for predecessor discovery via /resume @@ -557,6 +565,10 @@ func ensureCrewSession(t *tmux.Tmux, sessionName, crewPath, rigName, crewName st if err := t.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil { // Non-fatal } + + // Accept bypass permissions warning dialog if it appears. + _ = t.AcceptBypassPermissionsWarning(sessionName) + time.Sleep(constants.ShutdownNotifyDelay) // Inject startup nudge for predecessor discovery via /resume @@ -661,6 +673,10 @@ func ensurePolecatSession(t *tmux.Tmux, sessionName, polecatPath, rigName, polec if err := t.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil { // Non-fatal } + + // Accept bypass permissions warning dialog if it appears. + _ = t.AcceptBypassPermissionsWarning(sessionName) + time.Sleep(constants.ShutdownNotifyDelay) // Inject startup nudge for predecessor discovery via /resume diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 23c43138..49630163 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -695,6 +695,13 @@ func (d *Daemon) restartPolecatSession(rigName, polecatName, sessionName string) return fmt.Errorf("sending startup command: %w", err) } + // Wait for Claude to start, then accept bypass permissions warning if it appears. + // This ensures automated restarts aren't blocked by the warning dialog. + if err := d.tmux.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil { + // Non-fatal - Claude might still start + } + _ = d.tmux.AcceptBypassPermissionsWarning(sessionName) + return nil } diff --git a/internal/daemon/lifecycle.go b/internal/daemon/lifecycle.go index 2f794dd4..7f5e7bc2 100644 --- a/internal/daemon/lifecycle.go +++ b/internal/daemon/lifecycle.go @@ -364,6 +364,13 @@ func (d *Daemon) restartSession(sessionName, identity string) error { return fmt.Errorf("sending startup command: %w", err) } + // Wait for Claude to start, then accept bypass permissions warning if it appears. + // This ensures automated role starts aren't blocked by the warning dialog. + if err := d.tmux.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil { + // Non-fatal - Claude might still start + } + _ = d.tmux.AcceptBypassPermissionsWarning(sessionName) + // Note: gt prime is handled by Claude's SessionStart hook, not injected here. // Injecting it via SendKeysDelayed causes rogue text to appear in the terminal. diff --git a/internal/session/manager.go b/internal/session/manager.go index 5b85056f..2b153581 100644 --- a/internal/session/manager.go +++ b/internal/session/manager.go @@ -193,12 +193,18 @@ func (m *Manager) Start(polecat string, opts StartOptions) error { // Non-fatal warning - Claude might still start } + // Accept bypass permissions warning dialog if it appears. + // When Claude starts with --dangerously-skip-permissions, it shows a warning that + // requires pressing Down to select "Yes, I accept" and Enter to confirm. + // This is needed for automated polecat startup. + _ = m.tmux.AcceptBypassPermissionsWarning(sessionID) + // Wait for Claude to be fully ready at the prompt (not just started) // PRAGMATIC APPROACH: Use fixed delay rather than detection. // WaitForClaudeReady has false positives (detects > in various contexts). // Claude startup takes ~5-8 seconds on typical machines. - // 10 second delay is conservative but reliable. - time.Sleep(10 * time.Second) + // Reduced from 10s to 8s since AcceptBypassPermissionsWarning already adds ~1.2s. + time.Sleep(8 * time.Second) // Inject startup nudge for predecessor discovery via /resume // This becomes the session title in Claude Code's session picker diff --git a/internal/tmux/tmux.go b/internal/tmux/tmux.go index 5f2a7522..2b1a0071 100644 --- a/internal/tmux/tmux.go +++ b/internal/tmux/tmux.go @@ -265,6 +265,46 @@ func (t *Tmux) NudgePane(pane, message string) error { return nil } +// AcceptBypassPermissionsWarning dismisses the Claude Code bypass permissions warning dialog. +// When Claude starts with --dangerously-skip-permissions, it shows a warning dialog that +// requires pressing Down arrow to select "Yes, I accept" and then Enter to confirm. +// This function checks if the warning is present before sending keys to avoid interfering +// with sessions that don't show the warning (e.g., already accepted or different config). +// +// Call this after starting Claude and waiting for it to initialize (WaitForCommand), +// but before sending any prompts. +func (t *Tmux) AcceptBypassPermissionsWarning(session string) error { + // Wait for the dialog to potentially render + time.Sleep(1 * time.Second) + + // Check if the bypass permissions warning is present + content, err := t.CapturePane(session, 30) + if err != nil { + return err + } + + // Look for the characteristic warning text + if !strings.Contains(content, "Bypass Permissions mode") { + // Warning not present, nothing to do + return nil + } + + // Press Down to select "Yes, I accept" (option 2) + if _, err := t.run("send-keys", "-t", session, "Down"); err != nil { + return err + } + + // Small delay to let selection update + time.Sleep(200 * time.Millisecond) + + // Press Enter to confirm + if _, err := t.run("send-keys", "-t", session, "Enter"); err != nil { + return err + } + + return nil +} + // GetPaneCommand returns the current command running in a pane. // Returns "bash", "zsh", "claude", "node", etc. func (t *Tmux) GetPaneCommand(session string) (string, error) {