From 44cb5215b07165e3e7d3f011d868362b7d786b99 Mon Sep 17 00:00:00 2001 From: clawbot Date: Wed, 14 Oct 2026 21:12:58 +0200 Subject: [PATCH] Turn IdentitiesOnly off for ssh to (closes #74) ssh to now gives ssh -o IdentitiesOnly=no before the host, next to -o IdentityAgent. With IdentitiesOnly yes in the user's ssh configuration, ssh listed the agent's key but never offered it, and the login failed with Permission denied. An option on the command line wins over the configuration, and the tool's agent holds only the derived key, so turning the option off adds no other key. A test against the stand-in ssh requires the option before the host, and the README's ssh to section names both options. Judgement call: a user's own -o IdentitiesOnly=yes after the host is now overridden. Model: opus-5-5 Co-authored-by: clawbot --- README.md | 14 +++++++++----- internal/cli/ssh/to.go | 5 +++++ internal/cli/ssh_test.go | 22 +++++++++++++++++++++- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4560d6e..1b741f3 100644 --- a/README.md +++ b/README.md @@ -225,11 +225,15 @@ first, or pass `-o StrictHostKeyChecking=accept-new` after `--`. Derives the key, serves it from an SSH agent that runs inside the tool on a unix socket in a new private `0700` temporary directory, then runs the system `ssh` -with `-o IdentityAgent=` followed by the host and all remaining -arguments unchanged. The tool exits with `ssh`'s exit status and removes the -socket and directory on the way out. The private key is never written to disk. A -SIGINT, SIGTERM or SIGHUP ends `ssh` and still removes the socket and directory, -and the tool then exits with status 1 unless `ssh` reported one of its own. +with `-o IdentityAgent= -o IdentitiesOnly=no` followed by the host +and all remaining arguments unchanged. Without `IdentitiesOnly=no`, an +`IdentitiesOnly yes` in the user's `ssh` configuration keeps the derived key +from being offered. The agent holds no other key, so turning it off adds nothing +else to what `ssh` offers. The tool exits with `ssh`'s exit status and removes +the socket and directory on the way out. The private key is never written to +disk. A SIGINT, SIGTERM or SIGHUP ends `ssh` and still removes the socket and +directory, and the tool then exits with status 1 unless `ssh` reported one of +its own. ## age identities: `keyfunc age` diff --git a/internal/cli/ssh/to.go b/internal/cli/ssh/to.go index 9586800..69fc806 100644 --- a/internal/cli/ssh/to.go +++ b/internal/cli/ssh/to.go @@ -58,8 +58,13 @@ func to() *cobra.Command { defer served.Stop() + // IdentitiesOnly yes in the user's ssh configuration + // would keep the agent's key from being offered. The + // agent holds only the derived key, so turning it off + // adds no other key. argv := slices.Concat([]string{ "-o", "IdentityAgent=" + served.Socket(), + "-o", "IdentitiesOnly=no", }, args) return connect(ctx, argv) diff --git a/internal/cli/ssh_test.go b/internal/cli/ssh_test.go index 3aa8ef8..a669286 100644 --- a/internal/cli/ssh_test.go +++ b/internal/cli/ssh_test.go @@ -587,7 +587,7 @@ func TestSSHIsPointedAtTheAgentAndItsStatusIsHandedOn(t *testing.T) { given := recorded(t, arguments) require.Equal(t, "-o", given[0]) - require.Equal(t, []string{host, remoteCommand}, given[2:]) + require.Equal(t, []string{host, remoteCommand}, given[4:]) // The stand-in wrote the path down only because there really was // a socket there while it ran. @@ -596,6 +596,26 @@ func TestSSHIsPointedAtTheAgentAndItsStatusIsHandedOn(t *testing.T) { require.NoDirExists(t, filepath.Dir(socket)) } +func TestIdentitiesOnlyIsTurnedOffBeforeTheHost(t *testing.T) { + t.Setenv(mnemonic.Variable, example()) + + arguments, _ := pretendCall(t) + + _, err := execute(t, subcommand, "to", host, remoteCommand) + + var passed ssh.StatusError + + require.ErrorAs(t, err, &passed) + + // IdentitiesOnly yes in the user's ssh configuration keeps ssh + // from offering the agent's key. An option given before the host + // wins over the configuration. + given := recorded(t, arguments) + require.Equal(t, + []string{"-o", "IdentitiesOnly=no", host, remoteCommand}, given[2:], + ) +} + func TestTheToolEndsWithTheStatusSSHEndedWith(t *testing.T) { t.Setenv(mnemonic.Variable, example())