From 81122f86074a03fec708dcb3b6c103dab7c96fdb Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Wed, 24 Jun 2026 10:17:13 +0000 Subject: [PATCH] feat(cli): TTY-aware return + OSC52 clipboard with terminal gating --- cli/cmd_vault.go | 38 ++++++++++++++++++++++++++++++++++++++ cli/cmd_vault_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/cli/cmd_vault.go b/cli/cmd_vault.go index 4ee2ad47..84ba5dd9 100644 --- a/cli/cmd_vault.go +++ b/cli/cmd_vault.go @@ -1,6 +1,7 @@ package main import ( + "encoding/base64" "fmt" "os" "os/exec" @@ -144,6 +145,43 @@ func bwGet(run cmdRunner, env []string, field, name string) (string, error) { return run("bw", bwGetArgs(field, name), env) } +func returnMode(isTTY bool) string { + if isTTY { + return "clipboard" + } + return "stdout" +} + +// stdoutIsTTY reports whether stdout is a character device (a terminal). +func stdoutIsTTY() bool { + fi, err := os.Stdout.Stat() + if err != nil { + return false + } + return fi.Mode()&os.ModeCharDevice != 0 +} + +// osc52 returns the OSC 52 escape that makes the local terminal copy payload to +// the system clipboard (works over SSH; no X11). osc52clear copies empty. +func osc52(payload string) string { + return "\x1b]52;c;" + base64.StdEncoding.EncodeToString([]byte(payload)) + "\a" +} +func osc52clear() string { return "\x1b]52;c;\a" } + +// terminalAllowed gates OSC 52: only terminals known to honor clipboard writes, +// else we'd dump the secret's base64 into scrollback on unsupported terminals. +func terminalAllowed(term, termProgram string) bool { + t := strings.ToLower(term) + p := strings.ToLower(termProgram) + for _, ok := range []string{"kitty", "alacritty", "foot", "wezterm", "ghostty", "tmux", "screen"} { + if strings.Contains(t, ok) || strings.Contains(p, ok) { + return true + } + } + // xterm proper supports it only when the program is a known-good emulator. + return false +} + func vaultSetup(args []string) error { return fmt.Errorf("not implemented") } func vaultStatus(args []string) error { return fmt.Errorf("not implemented") } func vaultList(args []string) error { return fmt.Errorf("not implemented") } diff --git a/cli/cmd_vault_test.go b/cli/cmd_vault_test.go index 52a91a47..30fd540d 100644 --- a/cli/cmd_vault_test.go +++ b/cli/cmd_vault_test.go @@ -1,6 +1,7 @@ package main import ( + "encoding/base64" "reflect" "strings" "testing" @@ -140,3 +141,38 @@ func TestBwUnlockReturnsSession(t *testing.T) { t.Fatalf("unlock argv = %v", last) } } + +func TestReturnMode(t *testing.T) { + if returnMode(true) != "clipboard" || returnMode(false) != "stdout" { + t.Fatal("returnMode wrong") + } +} + +func TestOSC52Encode(t *testing.T) { + got := osc52("secret") + want := "\x1b]52;c;" + base64.StdEncoding.EncodeToString([]byte("secret")) + "\a" + if got != want { + t.Fatalf("osc52 = %q want %q", got, want) + } + if osc52clear() != "\x1b]52;c;\a" { + t.Fatalf("osc52clear wrong: %q", osc52clear()) + } +} + +func TestTerminalAllowed(t *testing.T) { + allow := []struct{ term, prog string }{ + {"xterm-kitty", ""}, {"alacritty", ""}, {"foot", ""}, {"tmux-256color", ""}, + {"screen-256color", ""}, {"xterm-256color", "WezTerm"}, {"xterm-256color", "ghostty"}, + } + for _, c := range allow { + if !terminalAllowed(c.term, c.prog) { + t.Errorf("terminalAllowed(%q,%q) = false, want true", c.term, c.prog) + } + } + deny := []struct{ term, prog string }{{"dumb", ""}, {"", ""}, {"vt100", ""}} + for _, c := range deny { + if terminalAllowed(c.term, c.prog) { + t.Errorf("terminalAllowed(%q,%q) = true, want false", c.term, c.prog) + } + } +}