From dd72a6e186897d45edfa5ff24f105b3271115aa8 Mon Sep 17 00:00:00 2001 From: webster Date: Fri, 15 May 2026 12:57:14 -0400 Subject: [PATCH 1/2] feat(config): add useSSHIdentityFile user config option Long-running Claude sessions would get stuck waiting on SSH_AUTH_SOCK approval prompts for each sidecar connection. Add a user-settable boolean (chunk config set useSSHIdentityFile true) that prefers the default chunk identity file (~/.ssh/chunk_ai) over the SSH agent, avoiding repeated auth prompts in hook context. Also print a warning when remote validate commands fail silently so the error is visible rather than just causing an unexplained exit code 2. Co-Authored-By: Claude Sonnet 4.6 --- internal/cmd/config.go | 24 +++++++++++++++++++++--- internal/cmd/validate.go | 13 ++++++++++++- internal/config/config.go | 12 +++++++----- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/internal/cmd/config.go b/internal/cmd/config.go index a8c01d6..2cc3f23 100644 --- a/internal/cmd/config.go +++ b/internal/cmd/config.go @@ -82,6 +82,10 @@ func newConfigShowCmd() *cobra.Command { io.Printf("%s %s\n", ui.Label("gitHubToken:", w), ui.Dim("(not set)")) } + if userCfg, err := config.Load(); err == nil { + io.Printf("%s %v\n", ui.Label("useSSHIdentityFile:", w), userCfg.UseSSHIdentityFile) + } + return nil }, } @@ -95,7 +99,7 @@ func newConfigSetCmd() *cobra.Command { return &cobra.Command{ Use: "set ", Short: "Set a config value", - Long: "Set a config value. Use 'chunk auth set ' to store credentials with validation.\n\nUser keys: model\nProject keys: orgID, validation.sidecarImage", + Long: "Set a config value. Use 'chunk auth set ' to store credentials with validation.\n\nUser keys: model, useSSHIdentityFile\nProject keys: orgID, validation.sidecarImage", Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { io := iostream.FromCmd(cmd) @@ -131,7 +135,7 @@ func newConfigSetCmd() *cobra.Command { if !config.ValidConfigKeys[key] { return &userError{ msg: fmt.Sprintf("Unknown config key: %q.", key), - detail: "Supported keys: model, orgID, validation.sidecarImage.", + detail: "Supported keys: model, useSSHIdentityFile, orgID, validation.sidecarImage.", errMsg: fmt.Sprintf("unknown config key %q", key), } } @@ -141,8 +145,22 @@ func newConfigSetCmd() *cobra.Command { return &userError{msg: msgCouldNotLoadConfig, suggestion: configFilePermHint, err: err} } - if key == "model" { + switch key { + case "model": cfg.Model = value + case "useSSHIdentityFile": + switch value { + case "true", "1": + cfg.UseSSHIdentityFile = true + case "false", "0": + cfg.UseSSHIdentityFile = false + default: + return &userError{ + msg: fmt.Sprintf("Invalid value %q for useSSHIdentityFile.", value), + detail: "Accepted values: true, false.", + errMsg: fmt.Sprintf("invalid boolean value %q", value), + } + } } if err := config.Save(cfg); err != nil { diff --git a/internal/cmd/validate.go b/internal/cmd/validate.go index 72e3b7c..9280a33 100644 --- a/internal/cmd/validate.go +++ b/internal/cmd/validate.go @@ -310,7 +310,15 @@ func openSSHSession(ctx context.Context, sidecarID, identityFile, workdir string if err != nil { return nil, "", err } - authSock := os.Getenv(config.EnvSSHAuthSock) + if identityFile == "" { + if userCfg, err := config.Load(); err == nil && userCfg.UseSSHIdentityFile { + identityFile, _ = sidecar.DefaultKeyPath() + } + } + var authSock string + if identityFile == "" { + authSock = os.Getenv(config.EnvSSHAuthSock) + } session, err := sidecar.OpenSession(ctx, client, sidecarID, identityFile, authSock) if err != nil { return nil, "", &userError{msg: "Could not open SSH session to sidecar.", err: err} @@ -366,6 +374,9 @@ func runSplitCommands(ctx context.Context, sidecarID string, freshlyCreated bool localCfg.Commands = append(remoteCfg.Commands, localCfg.Commands...) } else { runErr = validate.RunRemote(ctx, execFn, remoteCfg, "", dest, statusFn, streams) + if runErr != nil { + streams.ErrPrintf("warning: remote %s: %v\n", commandNames(remoteCfg.Commands), runErr) + } } } if len(localCfg.Commands) > 0 { diff --git a/internal/config/config.go b/internal/config/config.go index 3d4f530..e543cf9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -99,10 +99,11 @@ func LoadEnv(ctx context.Context) (EnvVars, error) { // UserConfig is the on-disk JSON config. type UserConfig struct { - AnthropicAPIKey string `json:"anthropicAPIKey,omitempty"` - CircleCIToken string `json:"circleCIToken,omitempty"` - GitHubToken string `json:"gitHubToken,omitempty"` - Model string `json:"model,omitempty"` + AnthropicAPIKey string `json:"anthropicAPIKey,omitempty"` + CircleCIToken string `json:"circleCIToken,omitempty"` + GitHubToken string `json:"gitHubToken,omitempty"` + Model string `json:"model,omitempty"` + UseSSHIdentityFile bool `json:"useSSHIdentityFile,omitempty"` // LegacyAPIKey reads the pre-rename "apiKey" field so existing users don't // silently lose their stored Anthropic key on upgrade. Migrated into @@ -273,7 +274,8 @@ func MaskKey(key string) string { // Credentials (anthropicAPIKey, circleCIToken) are intentionally excluded — // users should use "auth set" which validates before storing. var ValidConfigKeys = map[string]bool{ - "model": true, + "model": true, + "useSSHIdentityFile": true, } // ValidProjectConfigKeys are the keys accepted by "config set" that write to From dfc9a0399c8d6b9d22e57173a5a5bb34af09a2be Mon Sep 17 00:00:00 2001 From: webster Date: Fri, 15 May 2026 15:24:21 -0400 Subject: [PATCH 2/2] fix: surface UseSSHIdentityFile through ResolvedConfig and fix silent errors - Add UseSSHIdentityFile to ResolvedConfig so config show reuses the already-loaded config rather than calling config.Load() a second time with a silently swallowed error - Surface DefaultKeyPath() error as a warning instead of discarding it when useSSHIdentityFile is set - Remove misleading warning: print before remote run errors that are already returned as hard failures to the caller Co-Authored-By: Claude Sonnet 4.6 --- internal/cmd/config.go | 4 +--- internal/cmd/validate.go | 9 +++++---- internal/config/config.go | 2 ++ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/internal/cmd/config.go b/internal/cmd/config.go index 2cc3f23..10e0f34 100644 --- a/internal/cmd/config.go +++ b/internal/cmd/config.go @@ -82,9 +82,7 @@ func newConfigShowCmd() *cobra.Command { io.Printf("%s %s\n", ui.Label("gitHubToken:", w), ui.Dim("(not set)")) } - if userCfg, err := config.Load(); err == nil { - io.Printf("%s %v\n", ui.Label("useSSHIdentityFile:", w), userCfg.UseSSHIdentityFile) - } + io.Printf("%s %v\n", ui.Label("useSSHIdentityFile:", w), rc.UseSSHIdentityFile) return nil }, diff --git a/internal/cmd/validate.go b/internal/cmd/validate.go index 9280a33..651b0eb 100644 --- a/internal/cmd/validate.go +++ b/internal/cmd/validate.go @@ -312,7 +312,11 @@ func openSSHSession(ctx context.Context, sidecarID, identityFile, workdir string } if identityFile == "" { if userCfg, err := config.Load(); err == nil && userCfg.UseSSHIdentityFile { - identityFile, _ = sidecar.DefaultKeyPath() + var keyErr error + identityFile, keyErr = sidecar.DefaultKeyPath() + if keyErr != nil { + streams.ErrPrintf("warning: could not resolve SSH identity file: %v\n", keyErr) + } } } var authSock string @@ -374,9 +378,6 @@ func runSplitCommands(ctx context.Context, sidecarID string, freshlyCreated bool localCfg.Commands = append(remoteCfg.Commands, localCfg.Commands...) } else { runErr = validate.RunRemote(ctx, execFn, remoteCfg, "", dest, statusFn, streams) - if runErr != nil { - streams.ErrPrintf("warning: remote %s: %v\n", commandNames(remoteCfg.Commands), runErr) - } } } if len(localCfg.Commands) > 0 { diff --git a/internal/config/config.go b/internal/config/config.go index e543cf9..2964e04 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -126,6 +126,7 @@ type ResolvedConfig struct { ModelSource string AnalyzeModel string PromptModel string + UseSSHIdentityFile bool } // Load reads the config file. Returns empty config if not found. @@ -258,6 +259,7 @@ func Resolve(flagAPIKey, flagModel string) (ResolvedConfig, error) { rc.CircleCIBaseURL = env.CircleCIBaseURL rc.AnthropicBaseURL = env.AnthropicBaseURL rc.GitHubAPIURL = env.GitHubAPIURL + rc.UseSSHIdentityFile = cfg.UseSSHIdentityFile return rc, err }