π€ The native golang ssh client to execute your commands over ssh connection. ππ
See the code
Goph is a lightweight Go SSH client focusing on simplicity!
Features β Installation β Get Started β Usage Examples β License
Open, Create, Chmod... via SFTP.go get github.com/melbahja/goph/v2
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.
Expand each group then expand individual examples.
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("your_password"),
)
client, err := goph.New("root", "192.1.1.3",
goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
)
client, err := goph.New("root", "192.1.1.3",
goph.WithKeyFile("/home/user/.ssh/id_rsa", "passphrase"),
)
privateKey, _ := os.ReadFile("/home/user/.ssh/id_rsa")
client, err := goph.New("root", "192.1.1.3",
goph.WithKey(privateKey, "passphrase"),
)
if goph.HasAgent() {
client, err := goph.New("root", "192.1.1.3",
goph.WithDefaultAgent(),
)
}
client, err := goph.New("root", "192.1.1.3",
goph.WithAgentSocket("/run/user/1000/keyring/ssh"),
)
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),
)
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)
}),
)
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)
}),
)
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("try_password_first"),
goph.WithKeyFile("/home/user/.ssh/id_rsa", "passphrase"),
goph.WithDefaultAgent(),
)
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),
)
client, err := goph.New("root", "192.1.1.3",
goph.WithAuth(ssh.Password("pass")),
goph.WithAuth(ssh.PublicKeysCallback(myCustomCallback)),
)
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("pass"),
goph.WithPort(2222),
goph.WithTimeout(10*time.Second),
)
// 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"),
)
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("pass"),
goph.WithInsecureIgnoreHostKey(),
)
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),
)
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
}),
)
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"),
)
// 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),
)
// 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),
)
out, err := client.Run("ls /tmp/")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(out))
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")
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)
}
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)
}
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")
err := client.Upload("/path/to/local/file", "/path/to/remote/file")
err := client.Download("/path/to/remote/file", "/path/to/local/file")
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 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()
// 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"))
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
}),
)
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()
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"))
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
}
err := goph.AddKnownHost("myhost", remoteAddr, publicKey, "")
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),
)
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),
)
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")
}
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),
)
goph.WithInsecureIgnoreHostKey() in production; it makes you vulnerable to MITM attacks.~/.ssh/known_hosts file is created automatically as an empty file, but unknown host keys are still rejected until you explicitly trust them.Cmd.String() returns raw Path and Args. Never pass untrusted input directly into commands; sanitize or use fixed argument lists.goph.AddKnownHost. A mismatched key from goph.CheckKnownHost should be treated as a potential MITM attack.Expand each question to see the answer.
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")
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()
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))
Feel free to open a new issue, or contact me.
Goph is provided under the MIT License.
Overall
Maintained
Code review
Go module
Latest release
Releases this year
(top 24 of 27)
272 followers Β· starred Jul 2026
675 followers Β· starred Apr 2020
102 followers Β· starred Oct 2022
2,910 followers Β· starred Apr 2020
π€ The native golang ssh client to execute your commands over ssh connection. ππ
See the code
Goph is a lightweight Go SSH client focusing on simplicity!
Features β Installation β Get Started β Usage Examples β License
Open, Create, Chmod... via SFTP.go get github.com/melbahja/goph/v2
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.
Expand each group then expand individual examples.
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("your_password"),
)
client, err := goph.New("root", "192.1.1.3",
goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
)
client, err := goph.New("root", "192.1.1.3",
goph.WithKeyFile("/home/user/.ssh/id_rsa", "passphrase"),
)
privateKey, _ := os.ReadFile("/home/user/.ssh/id_rsa")
client, err := goph.New("root", "192.1.1.3",
goph.WithKey(privateKey, "passphrase"),
)
if goph.HasAgent() {
client, err := goph.New("root", "192.1.1.3",
goph.WithDefaultAgent(),
)
}
client, err := goph.New("root", "192.1.1.3",
goph.WithAgentSocket("/run/user/1000/keyring/ssh"),
)
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),
)
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)
}),
)
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)
}),
)
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("try_password_first"),
goph.WithKeyFile("/home/user/.ssh/id_rsa", "passphrase"),
goph.WithDefaultAgent(),
)
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),
)
client, err := goph.New("root", "192.1.1.3",
goph.WithAuth(ssh.Password("pass")),
goph.WithAuth(ssh.PublicKeysCallback(myCustomCallback)),
)
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("pass"),
goph.WithPort(2222),
goph.WithTimeout(10*time.Second),
)
// 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"),
)
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("pass"),
goph.WithInsecureIgnoreHostKey(),
)
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),
)
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
}),
)
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"),
)
// 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),
)
// 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),
)
out, err := client.Run("ls /tmp/")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(out))
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")
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)
}
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)
}
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")
err := client.Upload("/path/to/local/file", "/path/to/remote/file")
err := client.Download("/path/to/remote/file", "/path/to/local/file")
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 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()
// 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"))
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
}),
)
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()
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"))
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
}
err := goph.AddKnownHost("myhost", remoteAddr, publicKey, "")
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),
)
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),
)
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")
}
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),
)
goph.WithInsecureIgnoreHostKey() in production; it makes you vulnerable to MITM attacks.~/.ssh/known_hosts file is created automatically as an empty file, but unknown host keys are still rejected until you explicitly trust them.Cmd.String() returns raw Path and Args. Never pass untrusted input directly into commands; sanitize or use fixed argument lists.goph.AddKnownHost. A mismatched key from goph.CheckKnownHost should be treated as a potential MITM attack.Expand each question to see the answer.
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")
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()
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))
Feel free to open a new issue, or contact me.
Goph is provided under the MIT License.
Overall
Maintained
Code review
Go module
Latest release
Releases this year
(top 24 of 27)
272 followers Β· starred Jul 2026
675 followers Β· starred Apr 2020
102 followers Β· starred Oct 2022
2,910 followers Β· starred Apr 2020