melbahja/goph

🀘 The native golang ssh client to execute your commands over ssh connection. πŸš€πŸš€

Go

2,037

109 commits

updated Sep 15, 2026

See the code

README

Golang SSH Client.

Fast and easy golang ssh client module.

Goph is a lightweight Go SSH client focusing on simplicity!

Features ❘ Installation ❘ Get Started ❘ Usage Examples ❘ License

🀘  Features

  • Easy to use and simple API
  • Supports known hosts by default.
  • Supports connections with passwords.
  • Supports connections with private keys.
  • Supports connections with protected private keys with passphrase.
  • Supports multiple auth methods in a single connection.
  • Supports upload files from local to remote.
  • Supports download files from remote to local.
  • Supports connections with ssh agent.
  • Supports connections with custom signers.
  • Supports adding new hosts to known_hosts file.
  • Supports host key callback check from default known_hosts file.
  • Supports file operations like: Open, Create, Chmod... via SFTP.
  • Supports context.Context for command cancellation.
  • Supports SOCKS5 proxy for connecting through intermediaries.
  • Supports proxy jump for connecting through jump hosts.

πŸš€Β  Installation

go get github.com/melbahja/goph/v2

πŸ“„Β  Get Started

Run a command via ssh can be simple as this example:

package main

import (
	"log"
	"fmt"
	"github.com/melbahja/goph/v2"
)

func main() {

	// Start new ssh connection with private key.
	client, err := goph.New("root", "192.1.1.3", goph.WithPassword("try_password_first"))
	if err != nil {
		log.Fatal(err)
	}

	// Defer closing the network connection.
	defer client.Close()

	// Execute your command.
	out, err := client.Run("ls /tmp/")

	if err != nil {
		log.Fatal(err)
	}

	// Get your output as []byte.
	fmt.Println(string(out))
}

Docs are available in Go Docs.

πŸ“šΒ  Usage Examples

Expand each group then expand individual examples.

πŸ”Β  Authentication Examples

Password Authentication
client, err := goph.New("root", "192.1.1.3",
	goph.WithPassword("your_password"),
)
Key File Authentication
client, err := goph.New("root", "192.1.1.3",
	goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
)
Protected Private Key (with Passphrase)
client, err := goph.New("root", "192.1.1.3",
	goph.WithKeyFile("/home/user/.ssh/id_rsa", "passphrase"),
)
Raw Private Key (from bytes)
privateKey, _ := os.ReadFile("/home/user/.ssh/id_rsa")
client, err := goph.New("root", "192.1.1.3",
	goph.WithKey(privateKey, "passphrase"),
)
SSH Agent (auto-detect)
if goph.HasAgent() {
	client, err := goph.New("root", "192.1.1.3",
		goph.WithDefaultAgent(),
	)
}
Custom Agent Socket Path
client, err := goph.New("root", "192.1.1.3",
	goph.WithAgentSocket("/run/user/1000/keyring/ssh"),
)
Custom Agent Connection (net.Conn)
conn, err := net.Dial("unix", "/custom/agent.sock")
if err != nil {
	log.Fatal(err)
}

client, err := goph.New("root", "192.1.1.3",
	goph.WithAgent(conn),
)
Keyboard-Interactive (password prompt)
client, err := goph.New("root", "192.1.1.3",
	goph.WithKeyboardInteractive(func(user, instruction, question string, echo bool) (string, error) {
		if strings.Contains(strings.ToLower(question), "password") {
			return "your_password", nil
		}
		return "", fmt.Errorf("unexpected prompt: %s", question)
	}),
)
Keyboard-Interactive OTP / 2FA
client, err := goph.New("root", "192.1.1.3",
	goph.WithKeyboardInteractive(func(user, instruction, question string, echo bool) (string, error) {
		if strings.Contains(strings.ToLower(question), "otp") ||
			strings.Contains(strings.ToLower(question), "verification code") {
			fmt.Print(question, " ")
			var code string
			fmt.Scanln(&code)
			return code, nil
		}
		return "", fmt.Errorf("unexpected prompt: %s", question)
	}),
)
Multiple Auth Methods
client, err := goph.New("root", "192.1.1.3",
	goph.WithPassword("try_password_first"),
	goph.WithKeyFile("/home/user/.ssh/id_rsa", "passphrase"),
	goph.WithDefaultAgent(),
)
Custom Signer (any ssh.Signer)
signer, err := ssh.NewSignerFromKey(myPrivateKey) // or from any source
if err != nil {
	log.Fatal(err)
}

client, err := goph.New("root", "192.1.1.3",
	goph.WithSigner(signer),
)
Custom Auth Method (ssh.AuthMethod)
client, err := goph.New("root", "192.1.1.3",
	goph.WithAuth(ssh.Password("pass")),
	goph.WithAuth(ssh.PublicKeysCallback(myCustomCallback)),
)

πŸ“‘Β  Features Examples

Custom Port and Timeout
client, err := goph.New("root", "192.1.1.3",
	goph.WithPassword("pass"),
	goph.WithPort(2222),
	goph.WithTimeout(10*time.Second),
)
Known Hosts Verification (Default)
// Known hosts verification is ON by default via ~/.ssh/known_hosts
client, err := goph.New("root", "192.1.1.3",
	goph.WithPassword("pass"),
)

// Or specify a custom known_hosts file:
client, err := goph.New("root", "192.1.1.3",
	goph.WithPassword("pass"),
	goph.WithKnownHosts("/path/to/known_hosts"),
)
Disable Host Key Verification (Insecure)
client, err := goph.New("root", "192.1.1.3",
	goph.WithPassword("pass"),
	goph.WithInsecureIgnoreHostKey(),
)
Custom Host Key Callback
hostKeyCallback := func(hostname string, remote net.Addr, key ssh.PublicKey) error {
	// Check the key against a database or prompt the user
	return nil
}

client, err := goph.New("root", "192.1.1.3",
	goph.WithPassword("pass"),
	goph.WithHostKeyCallback(hostKeyCallback),
)
Banner Callback
client, err := goph.New("root", "192.1.1.3",
	goph.WithPassword("pass"),
	goph.WithBannerCallback(func(message string) error {
		log.Printf("SSH Banner: %s", message)
		return nil
	}),
)
Connect Through SOCKS5 Proxy
client, err := goph.New("root", "target-host",
	goph.WithPassword("pass"),
	goph.WithProxy("socks5://127.0.0.1:1080"),
)

// With proxy authentication:
client, err = goph.New("root", "target-host",
	goph.WithPassword("pass"),
	goph.WithProxy("socks5://proxyuser:proxypass@127.0.0.1:1080"),
)
Connect Through Jump Host (Bastion)
// First connect to the jump host
jump, err := goph.New("jumpuser", "bastion.example.com",
	goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
	goph.WithPort(2222),
)
if err != nil {
	log.Fatal(err)
}
defer jump.Close()

// Then tunnel through it to the target
client, err := goph.New("root", "internal-host",
	goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
	goph.WithJump(jump),
)
Proxy + Jump Host Together
// Put the proxy on the JUMP client, then pass the jump to the target
jump, err := goph.New("jumpuser", "bastion.example.com",
	goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
	goph.WithProxy("socks5://127.0.0.1:1080"),
)
if err != nil {
	log.Fatal(err)
}
defer jump.Close()

client, err := goph.New("root", "internal-host",
	goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
	goph.WithJump(jump),
)
Run a Command
out, err := client.Run("ls /tmp/")
if err != nil {
	log.Fatal(err)
}
fmt.Println(string(out))
Run with Timeout (Context)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

// Sends SIGINT and returns error after 1 second
out, err := client.RunContext(ctx, "sleep 5")
Cancel from Another Goroutine
ctx, cancel := context.WithCancel(context.Background())
cmd, err := client.CommandContext(ctx, "sleep", "60")
if err != nil {
	log.Fatal(err)
}

// Cancel from another goroutine
go func() {
	time.Sleep(5 * time.Second)
	cancel()
}()

if err := cmd.Run(); err != nil {
	// context canceled
	fmt.Println("Command stopped:", err)
}
Custom Cancel Function (override SIGINT)
cmd, err := client.CommandContext(ctx, "sleep", "30")
if err != nil {
	log.Fatal(err)
}

// Send SIGKILL instead of the default SIGINT on cancellation
cmd.Cancel = func() error {
	return cmd.Signal(ssh.SIGKILL)
}

if err := cmd.Run(); err != nil {
	log.Fatal(err)
}
Using Cmd for Fine Control
cmd, err := client.Command("ls", "-alh", "/tmp")
if err != nil {
	log.Fatal(err)
}

// Set env vars (server must accept them)
cmd.Env = []string{"MY_VAR=MYVALUE"}

// Run (CombinedOutput, Output, Start, Wait also available)
err = cmd.Run()

// With context:
cmd, err = client.CommandContext(ctx, "ls", "-alh", "/tmp")
Upload File
err := client.Upload("/path/to/local/file", "/path/to/remote/file")
Download File
err := client.Download("/path/to/remote/file", "/path/to/local/file")
SFTP File Operations
sftp, err := client.NewSftp()
if err != nil {
	log.Fatal(err)
}
defer sftp.Close()

// Create a file
file, err := sftp.Create("/tmp/remote_file")
if err != nil {
	log.Fatal(err)
}
defer file.Close()

file.Write([]byte("Hello world"))

// Open existing file
f, _ := sftp.Open("/etc/hostname")
// ... read from f

// Other methods: MkdirAll, Remove, Rename, ReadDir, Stat, etc.
Execute a Script (streaming from io.Reader)
// Execute a script from any io.Reader (e.g. a string, a file handle, an HTTP body)
cmd, err := client.Script(ctx, strings.NewReader("echo hello"))
if err != nil {
	log.Fatal(err)
}
out, err := cmd.CombinedOutput()
Execute a Script File
// Read a local script file into memory and execute it on the remote host
cmd, err := client.ScriptFile(ctx, "/path/to/local/script.sh")
if err != nil {
	log.Fatal(err)
}

// Override the interpreter for a PHP script:
cmd, err = client.ScriptFile(ctx, "/path/to/script.php", goph.WithPath("/usr/bin/php"))

// Override the shell path (default: /bin/sh)
cmd, err = client.ScriptFile(ctx, "/path/to/local/script.sh", goph.WithPath("/bin/zsh"))
Override Config Before Dial
client, err := goph.New("root", "192.1.1.3",
	goph.WithPassword("pass"),
	goph.WithConfig(func(config *ssh.ClientConfig) error {
		config.Timeout = 30 * time.Second
		config.ClientVersion = "SSH-2.0-MyApp"
		return nil
	}),
)
Low-Level: Custom ssh.ClientConfig (Dial)
c := &goph.Client{
	User: "root",
	Addr: "192.1.1.3",
	Port: 22,
}

config := &ssh.ClientConfig{
	Auth:            []ssh.AuthMethod{ssh.Password("pass")},
	HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}

if err := goph.Dial(c, config); err != nil {
	log.Fatal(err)
}
defer c.Close()
Reusable Dialer (Multiple Connections)
d := goph.NewDialer(
	goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
	goph.WithPort(2222),
)

client1, _ := d.New("root", "host1")
client2, _ := d.New("root", "host2", goph.WithPassword("fallback"))

πŸ› οΈΒ  Utils and Helpers

Check if Host is Known
found, err := goph.CheckKnownHost("myhost", remoteAddr, publicKey, "")
if err != nil {
	// key mismatch! possible MITM attack!
	log.Fatal(err)
}
if !found {
	// host is new, ask user to trust it
}
Add Host to Known Hosts
err := goph.AddKnownHost("myhost", remoteAddr, publicKey, "")
Parse Private Key File
signer, err := goph.ParseKeyFile("/home/user/.ssh/id_rsa", "passphrase")
if err != nil {
	log.Fatal(err)
}

client, err := goph.New("root", "192.1.1.3",
	goph.WithSigner(signer),
)
Parse Raw Key (from bytes)
keyBytes, _ := os.ReadFile("/home/user/.ssh/id_rsa")
signer, err := goph.ParseKey(keyBytes, "passphrase")
if err != nil {
	log.Fatal(err)
}

client, err := goph.New("root", "192.1.1.3",
	goph.WithSigner(signer),
)
Check SSH Agent Availability
if goph.HasAgent() {
	fmt.Println("SSH agent is available at", os.Getenv("SSH_AUTH_SOCK"))
} else {
	fmt.Println("SSH agent not running or SSH_AUTH_SOCK not set")
}
Default Known Hosts Path and Ensure
path, err := goph.DefaultKnownHostsPath()
if err != nil {
	log.Fatal(err)
}
fmt.Println("Known hosts file:", path)

// EnsureKnownHosts returns a callback and creates the file (and ~/.ssh/)
// if it does not exist yet β€” useful when writing first-time tools.
cb, err := goph.EnsureKnownHosts(filepath.Join(os.Getenv("HOME"), ".ssh", "my_hosts"))
if err != nil {
	log.Fatal(err)
}

client, err := goph.New("root", "192.1.1.3",
	goph.WithPassword("pass"),
	goph.WithHostKeyCallback(cb),
)

πŸ›‘οΈΒ  Security Notes

  • Known-hosts verification is enabled by default. Do not use goph.WithInsecureIgnoreHostKey() in production; it makes you vulnerable to MITM attacks.
  • A missing ~/.ssh/known_hosts file is created automatically as an empty file, but unknown host keys are still rejected until you explicitly trust them.
  • Command arguments are not shell-escaped. Cmd.String() returns raw Path and Args. Never pass untrusted input directly into commands; sanitize or use fixed argument lists.
  • Prompt the user before calling goph.AddKnownHost. A mismatched key from goph.CheckKnownHost should be treated as a potential MITM attack.

❓  FAQ

Expand each question to see the answer.

Why is there no Dir field like os/exec.Cmd?

SSH exec requests do not support setting a working directory in the protocol. The server runs the command in the user's default shell context (typically their home directory). To run a command in a specific directory, prefix it:

out, err := client.Run("cd /var/log && ls -la")
How do I run sudo commands?

Either connect as root, or use sudo -S and feed the password through stdin. Be aware that sudo may require a TTY, which goph does not provide by default:

cmd, err := client.Command("sudo", "-S", "systemctl", "restart", "nginx")
if err != nil {
	log.Fatal(err)
}

cmd.Stdin = strings.NewReader("your_sudo_password\n")
out, err := cmd.CombinedOutput()
Does goph work on Windows?

Yes. goph is pure Go and works on Windows, Linux, and macOS. For Windows-specific key providers (CNG/CAPI, Pageant, named-pipe OpenSSH agent), build or obtain an ssh.Signer and use WithSigner:

signer, err := ssh.NewSignerFromKey(myPrivateKey)
client, err := goph.New("user", "host", goph.WithSigner(signer))

🀝  Missing a Feature?

Feel free to open a new issue, or contact me.

πŸ“˜Β  License

Goph is provided under the MIT License.

downloader
go
golang
golang-module
goph
hacktoberfest
remote-execution
sftp
ssh
ssh-agent
ssh-client
ssh-keys
unix-systems
uploader

melbahja/goph

🀘 The native golang ssh client to execute your commands over ssh connection. πŸš€πŸš€

Go

2,037

109 commits

updated Sep 15, 2026

See the code

README

Golang SSH Client.

Fast and easy golang ssh client module.

Goph is a lightweight Go SSH client focusing on simplicity!

Features ❘ Installation ❘ Get Started ❘ Usage Examples ❘ License

🀘  Features

  • Easy to use and simple API
  • Supports known hosts by default.
  • Supports connections with passwords.
  • Supports connections with private keys.
  • Supports connections with protected private keys with passphrase.
  • Supports multiple auth methods in a single connection.
  • Supports upload files from local to remote.
  • Supports download files from remote to local.
  • Supports connections with ssh agent.
  • Supports connections with custom signers.
  • Supports adding new hosts to known_hosts file.
  • Supports host key callback check from default known_hosts file.
  • Supports file operations like: Open, Create, Chmod... via SFTP.
  • Supports context.Context for command cancellation.
  • Supports SOCKS5 proxy for connecting through intermediaries.
  • Supports proxy jump for connecting through jump hosts.

πŸš€Β  Installation

go get github.com/melbahja/goph/v2

πŸ“„Β  Get Started

Run a command via ssh can be simple as this example:

package main

import (
	"log"
	"fmt"
	"github.com/melbahja/goph/v2"
)

func main() {

	// Start new ssh connection with private key.
	client, err := goph.New("root", "192.1.1.3", goph.WithPassword("try_password_first"))
	if err != nil {
		log.Fatal(err)
	}

	// Defer closing the network connection.
	defer client.Close()

	// Execute your command.
	out, err := client.Run("ls /tmp/")

	if err != nil {
		log.Fatal(err)
	}

	// Get your output as []byte.
	fmt.Println(string(out))
}

Docs are available in Go Docs.

πŸ“šΒ  Usage Examples

Expand each group then expand individual examples.

πŸ”Β  Authentication Examples

Password Authentication
client, err := goph.New("root", "192.1.1.3",
	goph.WithPassword("your_password"),
)
Key File Authentication
client, err := goph.New("root", "192.1.1.3",
	goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
)
Protected Private Key (with Passphrase)
client, err := goph.New("root", "192.1.1.3",
	goph.WithKeyFile("/home/user/.ssh/id_rsa", "passphrase"),
)
Raw Private Key (from bytes)
privateKey, _ := os.ReadFile("/home/user/.ssh/id_rsa")
client, err := goph.New("root", "192.1.1.3",
	goph.WithKey(privateKey, "passphrase"),
)
SSH Agent (auto-detect)
if goph.HasAgent() {
	client, err := goph.New("root", "192.1.1.3",
		goph.WithDefaultAgent(),
	)
}
Custom Agent Socket Path
client, err := goph.New("root", "192.1.1.3",
	goph.WithAgentSocket("/run/user/1000/keyring/ssh"),
)
Custom Agent Connection (net.Conn)
conn, err := net.Dial("unix", "/custom/agent.sock")
if err != nil {
	log.Fatal(err)
}

client, err := goph.New("root", "192.1.1.3",
	goph.WithAgent(conn),
)
Keyboard-Interactive (password prompt)
client, err := goph.New("root", "192.1.1.3",
	goph.WithKeyboardInteractive(func(user, instruction, question string, echo bool) (string, error) {
		if strings.Contains(strings.ToLower(question), "password") {
			return "your_password", nil
		}
		return "", fmt.Errorf("unexpected prompt: %s", question)
	}),
)
Keyboard-Interactive OTP / 2FA
client, err := goph.New("root", "192.1.1.3",
	goph.WithKeyboardInteractive(func(user, instruction, question string, echo bool) (string, error) {
		if strings.Contains(strings.ToLower(question), "otp") ||
			strings.Contains(strings.ToLower(question), "verification code") {
			fmt.Print(question, " ")
			var code string
			fmt.Scanln(&code)
			return code, nil
		}
		return "", fmt.Errorf("unexpected prompt: %s", question)
	}),
)
Multiple Auth Methods
client, err := goph.New("root", "192.1.1.3",
	goph.WithPassword("try_password_first"),
	goph.WithKeyFile("/home/user/.ssh/id_rsa", "passphrase"),
	goph.WithDefaultAgent(),
)
Custom Signer (any ssh.Signer)
signer, err := ssh.NewSignerFromKey(myPrivateKey) // or from any source
if err != nil {
	log.Fatal(err)
}

client, err := goph.New("root", "192.1.1.3",
	goph.WithSigner(signer),
)
Custom Auth Method (ssh.AuthMethod)
client, err := goph.New("root", "192.1.1.3",
	goph.WithAuth(ssh.Password("pass")),
	goph.WithAuth(ssh.PublicKeysCallback(myCustomCallback)),
)

πŸ“‘Β  Features Examples

Custom Port and Timeout
client, err := goph.New("root", "192.1.1.3",
	goph.WithPassword("pass"),
	goph.WithPort(2222),
	goph.WithTimeout(10*time.Second),
)
Known Hosts Verification (Default)
// Known hosts verification is ON by default via ~/.ssh/known_hosts
client, err := goph.New("root", "192.1.1.3",
	goph.WithPassword("pass"),
)

// Or specify a custom known_hosts file:
client, err := goph.New("root", "192.1.1.3",
	goph.WithPassword("pass"),
	goph.WithKnownHosts("/path/to/known_hosts"),
)
Disable Host Key Verification (Insecure)
client, err := goph.New("root", "192.1.1.3",
	goph.WithPassword("pass"),
	goph.WithInsecureIgnoreHostKey(),
)
Custom Host Key Callback
hostKeyCallback := func(hostname string, remote net.Addr, key ssh.PublicKey) error {
	// Check the key against a database or prompt the user
	return nil
}

client, err := goph.New("root", "192.1.1.3",
	goph.WithPassword("pass"),
	goph.WithHostKeyCallback(hostKeyCallback),
)
Banner Callback
client, err := goph.New("root", "192.1.1.3",
	goph.WithPassword("pass"),
	goph.WithBannerCallback(func(message string) error {
		log.Printf("SSH Banner: %s", message)
		return nil
	}),
)
Connect Through SOCKS5 Proxy
client, err := goph.New("root", "target-host",
	goph.WithPassword("pass"),
	goph.WithProxy("socks5://127.0.0.1:1080"),
)

// With proxy authentication:
client, err = goph.New("root", "target-host",
	goph.WithPassword("pass"),
	goph.WithProxy("socks5://proxyuser:proxypass@127.0.0.1:1080"),
)
Connect Through Jump Host (Bastion)
// First connect to the jump host
jump, err := goph.New("jumpuser", "bastion.example.com",
	goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
	goph.WithPort(2222),
)
if err != nil {
	log.Fatal(err)
}
defer jump.Close()

// Then tunnel through it to the target
client, err := goph.New("root", "internal-host",
	goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
	goph.WithJump(jump),
)
Proxy + Jump Host Together
// Put the proxy on the JUMP client, then pass the jump to the target
jump, err := goph.New("jumpuser", "bastion.example.com",
	goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
	goph.WithProxy("socks5://127.0.0.1:1080"),
)
if err != nil {
	log.Fatal(err)
}
defer jump.Close()

client, err := goph.New("root", "internal-host",
	goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
	goph.WithJump(jump),
)
Run a Command
out, err := client.Run("ls /tmp/")
if err != nil {
	log.Fatal(err)
}
fmt.Println(string(out))
Run with Timeout (Context)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

// Sends SIGINT and returns error after 1 second
out, err := client.RunContext(ctx, "sleep 5")
Cancel from Another Goroutine
ctx, cancel := context.WithCancel(context.Background())
cmd, err := client.CommandContext(ctx, "sleep", "60")
if err != nil {
	log.Fatal(err)
}

// Cancel from another goroutine
go func() {
	time.Sleep(5 * time.Second)
	cancel()
}()

if err := cmd.Run(); err != nil {
	// context canceled
	fmt.Println("Command stopped:", err)
}
Custom Cancel Function (override SIGINT)
cmd, err := client.CommandContext(ctx, "sleep", "30")
if err != nil {
	log.Fatal(err)
}

// Send SIGKILL instead of the default SIGINT on cancellation
cmd.Cancel = func() error {
	return cmd.Signal(ssh.SIGKILL)
}

if err := cmd.Run(); err != nil {
	log.Fatal(err)
}
Using Cmd for Fine Control
cmd, err := client.Command("ls", "-alh", "/tmp")
if err != nil {
	log.Fatal(err)
}

// Set env vars (server must accept them)
cmd.Env = []string{"MY_VAR=MYVALUE"}

// Run (CombinedOutput, Output, Start, Wait also available)
err = cmd.Run()

// With context:
cmd, err = client.CommandContext(ctx, "ls", "-alh", "/tmp")
Upload File
err := client.Upload("/path/to/local/file", "/path/to/remote/file")
Download File
err := client.Download("/path/to/remote/file", "/path/to/local/file")
SFTP File Operations
sftp, err := client.NewSftp()
if err != nil {
	log.Fatal(err)
}
defer sftp.Close()

// Create a file
file, err := sftp.Create("/tmp/remote_file")
if err != nil {
	log.Fatal(err)
}
defer file.Close()

file.Write([]byte("Hello world"))

// Open existing file
f, _ := sftp.Open("/etc/hostname")
// ... read from f

// Other methods: MkdirAll, Remove, Rename, ReadDir, Stat, etc.
Execute a Script (streaming from io.Reader)
// Execute a script from any io.Reader (e.g. a string, a file handle, an HTTP body)
cmd, err := client.Script(ctx, strings.NewReader("echo hello"))
if err != nil {
	log.Fatal(err)
}
out, err := cmd.CombinedOutput()
Execute a Script File
// Read a local script file into memory and execute it on the remote host
cmd, err := client.ScriptFile(ctx, "/path/to/local/script.sh")
if err != nil {
	log.Fatal(err)
}

// Override the interpreter for a PHP script:
cmd, err = client.ScriptFile(ctx, "/path/to/script.php", goph.WithPath("/usr/bin/php"))

// Override the shell path (default: /bin/sh)
cmd, err = client.ScriptFile(ctx, "/path/to/local/script.sh", goph.WithPath("/bin/zsh"))
Override Config Before Dial
client, err := goph.New("root", "192.1.1.3",
	goph.WithPassword("pass"),
	goph.WithConfig(func(config *ssh.ClientConfig) error {
		config.Timeout = 30 * time.Second
		config.ClientVersion = "SSH-2.0-MyApp"
		return nil
	}),
)
Low-Level: Custom ssh.ClientConfig (Dial)
c := &goph.Client{
	User: "root",
	Addr: "192.1.1.3",
	Port: 22,
}

config := &ssh.ClientConfig{
	Auth:            []ssh.AuthMethod{ssh.Password("pass")},
	HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}

if err := goph.Dial(c, config); err != nil {
	log.Fatal(err)
}
defer c.Close()
Reusable Dialer (Multiple Connections)
d := goph.NewDialer(
	goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
	goph.WithPort(2222),
)

client1, _ := d.New("root", "host1")
client2, _ := d.New("root", "host2", goph.WithPassword("fallback"))

πŸ› οΈΒ  Utils and Helpers

Check if Host is Known
found, err := goph.CheckKnownHost("myhost", remoteAddr, publicKey, "")
if err != nil {
	// key mismatch! possible MITM attack!
	log.Fatal(err)
}
if !found {
	// host is new, ask user to trust it
}
Add Host to Known Hosts
err := goph.AddKnownHost("myhost", remoteAddr, publicKey, "")
Parse Private Key File
signer, err := goph.ParseKeyFile("/home/user/.ssh/id_rsa", "passphrase")
if err != nil {
	log.Fatal(err)
}

client, err := goph.New("root", "192.1.1.3",
	goph.WithSigner(signer),
)
Parse Raw Key (from bytes)
keyBytes, _ := os.ReadFile("/home/user/.ssh/id_rsa")
signer, err := goph.ParseKey(keyBytes, "passphrase")
if err != nil {
	log.Fatal(err)
}

client, err := goph.New("root", "192.1.1.3",
	goph.WithSigner(signer),
)
Check SSH Agent Availability
if goph.HasAgent() {
	fmt.Println("SSH agent is available at", os.Getenv("SSH_AUTH_SOCK"))
} else {
	fmt.Println("SSH agent not running or SSH_AUTH_SOCK not set")
}
Default Known Hosts Path and Ensure
path, err := goph.DefaultKnownHostsPath()
if err != nil {
	log.Fatal(err)
}
fmt.Println("Known hosts file:", path)

// EnsureKnownHosts returns a callback and creates the file (and ~/.ssh/)
// if it does not exist yet β€” useful when writing first-time tools.
cb, err := goph.EnsureKnownHosts(filepath.Join(os.Getenv("HOME"), ".ssh", "my_hosts"))
if err != nil {
	log.Fatal(err)
}

client, err := goph.New("root", "192.1.1.3",
	goph.WithPassword("pass"),
	goph.WithHostKeyCallback(cb),
)

πŸ›‘οΈΒ  Security Notes

  • Known-hosts verification is enabled by default. Do not use goph.WithInsecureIgnoreHostKey() in production; it makes you vulnerable to MITM attacks.
  • A missing ~/.ssh/known_hosts file is created automatically as an empty file, but unknown host keys are still rejected until you explicitly trust them.
  • Command arguments are not shell-escaped. Cmd.String() returns raw Path and Args. Never pass untrusted input directly into commands; sanitize or use fixed argument lists.
  • Prompt the user before calling goph.AddKnownHost. A mismatched key from goph.CheckKnownHost should be treated as a potential MITM attack.

❓  FAQ

Expand each question to see the answer.

Why is there no Dir field like os/exec.Cmd?

SSH exec requests do not support setting a working directory in the protocol. The server runs the command in the user's default shell context (typically their home directory). To run a command in a specific directory, prefix it:

out, err := client.Run("cd /var/log && ls -la")
How do I run sudo commands?

Either connect as root, or use sudo -S and feed the password through stdin. Be aware that sudo may require a TTY, which goph does not provide by default:

cmd, err := client.Command("sudo", "-S", "systemctl", "restart", "nginx")
if err != nil {
	log.Fatal(err)
}

cmd.Stdin = strings.NewReader("your_sudo_password\n")
out, err := cmd.CombinedOutput()
Does goph work on Windows?

Yes. goph is pure Go and works on Windows, Linux, and macOS. For Windows-specific key providers (CNG/CAPI, Pageant, named-pipe OpenSSH agent), build or obtain an ssh.Signer and use WithSigner:

signer, err := ssh.NewSignerFromKey(myPrivateKey)
client, err := goph.New("user", "host", goph.WithSigner(signer))

🀝  Missing a Feature?

Feel free to open a new issue, or contact me.

πŸ“˜Β  License

Goph is provided under the MIT License.

downloader
go
golang
golang-module
goph
hacktoberfest
remote-execution
sftp
ssh
ssh-agent
ssh-client
ssh-keys
unix-systems
uploader

Project health

Overall

4.1/10

Maintained

10/10

Code review

3/10
OpenSSF Scorecard

Latest release

v1.5.2 Β· Jul 15, 2026

Releases this year

3

Significant stargazers

(top 24 of 27)

Julian Ospald

272 followers Β· starred Jul 2026

Samuel Karp

675 followers Β· starred Apr 2020

Geoff Garside

102 followers Β· starred Oct 2022

Chris Lu

2,910 followers Β· starred Apr 2020