2026-06-24 10:14:24 +00:00
package main
2026-06-24 10:15:32 +00:00
import (
2026-06-24 10:21:57 +00:00
"bufio"
2026-06-24 10:17:13 +00:00
"encoding/base64"
2026-06-24 10:21:07 +00:00
"encoding/json"
2026-06-24 10:15:32 +00:00
"fmt"
"os"
"os/exec"
"strings"
2026-06-24 10:18:36 +00:00
"syscall"
2026-06-24 10:15:32 +00:00
)
2026-06-24 10:14:24 +00:00
// vault verbs give each unix user no-HITL access to THEIR OWN Vaultwarden vault.
// Identity is the kernel UID; per-user creds live in that user's isolated Vault
// path (secret/workstation/claude-users/<user>) read via their scoped token, and
// decryption is done by the official `bw` CLI. See
// docs/superpowers/specs/2026-06-24-homelab-vault-design.md.
func vaultCommands ( ) [ ] Command {
return [ ] Command {
{ Path : [ ] string { "vault" , "setup" } , Tier : TierWrite ,
Summary : "one-time: store your Vaultwarden master password + API key in your Vault path" , Run : vaultSetup } ,
{ Path : [ ] string { "vault" , "status" } , Tier : TierRead ,
Summary : "show whether your vault is configured/reachable (no secrets)" , Run : vaultStatus } ,
{ Path : [ ] string { "vault" , "list" } , Tier : TierRead ,
Summary : "list your item names: vault list [--search Q]" , Run : vaultList } ,
{ Path : [ ] string { "vault" , "get" } , Tier : TierRead ,
2026-06-24 10:28:31 +00:00
Summary : "fetch one item: vault get <name> [--field password|username|uri|notes|totp] [--json]" , Run : vaultGet } ,
2026-06-24 10:14:24 +00:00
{ Path : [ ] string { "vault" , "search" } , Tier : TierRead ,
Summary : "search your item names: vault search <query>" , Run : vaultSearch } ,
{ Path : [ ] string { "vault" , "code" } , Tier : TierRead ,
Summary : "current TOTP code for an item: vault code <name>" , Run : vaultCode } ,
{ Path : [ ] string { "vault" , "lock" } , Tier : TierWrite ,
Summary : "lock/log out the local bw session" , Run : vaultLock } ,
2026-06-24 10:29:32 +00:00
{ Path : [ ] string { "vault" } , Tier : TierRead ,
Summary : "Vaultwarden access for your own vault (run `homelab vault` for help)" ,
Run : func ( [ ] string ) error { fmt . Print ( vaultHelp ( ) ) ; return nil } } ,
2026-06-24 10:14:24 +00:00
}
}
2026-06-24 10:29:32 +00:00
// vaultHelp is shown for bare `homelab vault`.
func vaultHelp ( ) string {
return ` homelab vault — read YOUR OWN Vaultwarden logins ( no - HITL after one - time setup )
homelab vault setup one - time : store your master password + API key in your Vault path
homelab vault status configured / unlocked / reachable ( no secrets )
homelab vault list [ -- search Q ] list your item names ( no secrets )
homelab vault get < name > [ -- field password | username | uri | notes | totp ] [ -- json ]
TTY → clipboard ( auto - clears ) ; piped → stdout
homelab vault code < name > current TOTP code
homelab vault lock lock / log out the local bw session
Creds live only in your own Vault path ; the admin never sees them . Identity is
your unix UID . Security model : docs / superpowers / specs / 2026 - 06 - 24 - homelab - vault - design . md
( note : anything running as your user can decrypt your vault — the accepted no - HITL trade ) .
`
}
2026-06-24 10:15:32 +00:00
const vwUserPathPrefix = "secret/workstation/claude-users/"
// vwCreds is one user's Vaultwarden auth material, read from their Vault path.
type vwCreds struct {
Email string
MasterPassword string
ClientID string
ClientSecret string
}
// cmdRunner shells out to an external command with an explicit environment and
// returns trimmed stdout. Secrets are passed via envv, NEVER argv. Tests inject
// a fake; realRunner is the production implementation.
type cmdRunner func ( name string , argv , envv [ ] string ) ( string , error )
func realRunner ( name string , argv , envv [ ] string ) ( string , error ) {
cmd := exec . Command ( name , argv ... )
if envv != nil {
cmd . Env = envv
}
out , err := cmd . Output ( )
2026-06-24 10:28:31 +00:00
// Trim only the trailing newline the tool appends — NOT all whitespace, so a
// fetched secret with significant leading/trailing spaces is preserved.
return strings . TrimRight ( string ( out ) , "\r\n" ) , err
}
// realRunnerStdin runs a command feeding `stdin` to it, for secret values that
// must NOT appear in argv (visible via ps / /proc/<pid>/cmdline to same-UID
// processes). Used by setup to write the master password / client_secret.
func realRunnerStdin ( name string , argv , envv [ ] string , stdin string ) ( string , error ) {
cmd := exec . Command ( name , argv ... )
if envv != nil {
cmd . Env = envv
}
cmd . Stdin = strings . NewReader ( stdin )
out , err := cmd . Output ( )
return strings . TrimRight ( string ( out ) , "\r\n" ) , err
2026-06-24 10:15:32 +00:00
}
func vwCredsPath ( user string ) string { return vwUserPathPrefix + user }
func bwAppDataDir ( uid string ) string { return "/run/user/" + uid + "/homelab-bw" }
// readVaultField returns one field from a KV-v2 path, "" if absent/error.
func readVaultField ( run cmdRunner , field , path string ) string {
out , err := run ( "vault" , [ ] string { "kv" , "get" , "-field=" + field , path } , nil )
if err != nil {
return ""
}
return out
}
// loadCreds reads the four vaultwarden_* keys from the user's isolated path.
// A missing master password means the user hasn't onboarded.
func loadCreds ( run cmdRunner , user string ) ( vwCreds , error ) {
p := vwCredsPath ( user )
c := vwCreds {
Email : readVaultField ( run , "vaultwarden_email" , p ) ,
MasterPassword : readVaultField ( run , "vaultwarden_master_password" , p ) ,
ClientID : readVaultField ( run , "vaultwarden_client_id" , p ) ,
ClientSecret : readVaultField ( run , "vaultwarden_client_secret" , p ) ,
}
if c . MasterPassword == "" {
return vwCreds { } , fmt . Errorf ( "vault not configured for this user — run `homelab vault setup`" )
}
return c , nil
}
// vaultCurrentUser/vaultCurrentUID are seams for tests (avoid conflict with repo.go's currentUser func).
var vaultCurrentUser = func ( ) string { return os . Getenv ( "USER" ) }
var vaultCurrentUID = func ( ) string { return fmt . Sprintf ( "%d" , os . Getuid ( ) ) }
2026-06-24 10:16:19 +00:00
// bwBaseEnv is the minimal non-secret environment bw/node need. We deliberately
// do NOT inherit the full parent env (keeps stray secrets out of the child).
func bwBaseEnv ( appdata string ) [ ] string {
path := os . Getenv ( "PATH" )
if path == "" {
path = "/usr/local/bin:/usr/bin:/bin"
}
return [ ] string {
"PATH=" + path ,
"HOME=" + os . Getenv ( "HOME" ) ,
"BITWARDENCLI_APPDATA_DIR=" + appdata ,
"BW_NOINTERACTION=true" ,
}
}
// bwSecretEnv adds the secret-bearing vars. session may be "" (pre-unlock).
func bwSecretEnv ( appdata string , c vwCreds , session string ) [ ] string {
env := bwBaseEnv ( appdata )
env = append ( env ,
"BW_CLIENTID=" + c . ClientID ,
"BW_CLIENTSECRET=" + c . ClientSecret ,
"BW_PASSWORD=" + c . MasterPassword ,
)
if session != "" {
env = append ( env , "BW_SESSION=" + session )
}
return env
}
func bwLoginArgs ( ) [ ] string { return [ ] string { "login" , "--apikey" } }
func bwUnlockArgs ( ) [ ] string { return [ ] string { "unlock" , "--passwordenv" , "BW_PASSWORD" , "--raw" } }
func bwGetArgs ( field , name string ) [ ] string { return [ ] string { "get" , field , name } }
func bwStatusArgs ( ) [ ] string { return [ ] string { "status" } }
2026-06-24 10:28:31 +00:00
// bwNeedsLogin parses `bw status` JSON and reports whether a `bw login` is
// required. Unparseable/empty output → true (safer to attempt login).
func bwNeedsLogin ( statusJSON string ) bool {
var s struct {
Status string ` json:"status" `
}
if err := json . Unmarshal ( [ ] byte ( statusJSON ) , & s ) ; err != nil {
return true
}
return s . Status == "unauthenticated" || s . Status == ""
}
2026-06-24 10:16:19 +00:00
func bwListArgs ( search string ) [ ] string {
a := [ ] string { "list" , "items" }
if search != "" {
a = append ( a , "--search" , search )
}
return a
}
// bwUnlock runs `bw unlock` and returns the raw session key.
func bwUnlock ( run cmdRunner , env [ ] string ) ( string , error ) {
out , err := run ( "bw" , bwUnlockArgs ( ) , env )
if err != nil {
return "" , fmt . Errorf ( "bw unlock failed (wrong master password? run `homelab vault setup`): %w" , err )
}
return out , nil
}
// bwGet fetches one field of one item; session must be present in env.
func bwGet ( run cmdRunner , env [ ] string , field , name string ) ( string , error ) {
return run ( "bw" , bwGetArgs ( field , name ) , env )
}
2026-06-24 10:17:13 +00:00
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
}
2026-06-24 10:28:31 +00:00
// stderrIsTTY reports whether stderr is a terminal (the OSC52 escape is written
// to stderr, so the clipboard path is only viable when stderr is a terminal).
func stderrIsTTY ( ) bool {
fi , err := os . Stderr . Stat ( )
if err != nil {
return false
}
return fi . Mode ( ) & os . ModeCharDevice != 0
}
2026-06-24 10:17:13 +00:00
// 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
}
2026-06-24 10:17:50 +00:00
// opRecord is one CLI operation. ItemName is accepted for the caller's
// convenience but is INTENTIONALLY never rendered into the log line — auditing
// which of your own logins you opened is itself sensitive, and per-item reads
// are invisible server-side anyway (spec §9a).
type opRecord struct {
User string
Verb string
PID int
PPID int
ParentComm string
ItemName string // never logged
}
func opLogLine ( r opRecord ) string {
return fmt . Sprintf ( "user=%s verb=%s pid=%d ppid=%d parent=%s" ,
r . User , r . Verb , r . PID , r . PPID , r . ParentComm )
}
// parentComm reads /proc/<ppid>/comm (best-effort; "" on failure).
func parentComm ( ppid int ) string {
b , err := os . ReadFile ( fmt . Sprintf ( "/proc/%d/comm" , ppid ) )
if err != nil {
return ""
}
return strings . TrimSpace ( string ( b ) )
}
// writeOpLog appends one privacy-aware line to the user's op-log (best-effort;
// never blocks or fails the command). Goes to syslog so it ships to Loki.
func writeOpLog ( r opRecord ) {
exec . Command ( "logger" , "-t" , "homelab-vault" , opLogLine ( r ) ) . Run ( ) // best-effort
}
2026-06-24 10:18:36 +00:00
func vaultLockPath ( uid string ) string { return "/run/user/" + uid + "/homelab-vault.lock" }
// hardenProcess disables core dumps so a bw/homelab crash can't spill the master
// password to a core file. Best-effort.
func hardenProcess ( ) {
_ = syscall . Setrlimit ( syscall . RLIMIT_CORE , & syscall . Rlimit { Cur : 0 , Max : 0 } )
}
// withUserLock serializes bw mutations for this user (concurrent Claude sessions
// as the same user otherwise race bw's appdata). Returns an unlock func.
func withUserLock ( uid string ) ( func ( ) , error ) {
f , err := os . OpenFile ( vaultLockPath ( uid ) , os . O_CREATE | os . O_RDWR , 0600 )
if err != nil {
return nil , err
}
if err := syscall . Flock ( int ( f . Fd ( ) ) , syscall . LOCK_EX ) ; err != nil {
f . Close ( )
return nil , err
}
return func ( ) { syscall . Flock ( int ( f . Fd ( ) ) , syscall . LOCK_UN ) ; f . Close ( ) } , nil
}
// session is one usable bw context: the env (with BW_SESSION) ready for `bw get`.
type session struct {
env [ ] string
}
// openSession resolves creds, ensures login, unlocks, and returns a ready env.
// Caller must hold the user lock. appdata is created on tmpfs (0700).
func openSession ( run cmdRunner , user , uid string ) ( session , error ) {
creds , err := loadCreds ( run , user )
if err != nil {
return session { } , err
}
appdata := bwAppDataDir ( uid )
if err := os . MkdirAll ( appdata , 0700 ) ; err != nil {
return session { } , fmt . Errorf ( "create bw appdata %s: %w" , appdata , err )
}
loginEnv := bwSecretEnv ( appdata , creds , "" )
// Ensure server is set and we're logged in (idempotent; ignore "already").
_ , _ = run ( "bw" , [ ] string { "config" , "server" , "https://vaultwarden.viktorbarzin.me" } , loginEnv )
2026-06-24 10:28:31 +00:00
st , _ := run ( "bw" , bwStatusArgs ( ) , loginEnv )
if bwNeedsLogin ( st ) {
2026-06-24 10:18:36 +00:00
if _ , err := run ( "bw" , bwLoginArgs ( ) , loginEnv ) ; err != nil {
return session { } , fmt . Errorf ( "bw login --apikey failed (API key valid? run `homelab vault setup`): %w" , err )
}
}
sess , err := bwUnlock ( run , loginEnv )
if err != nil {
return session { } , err
}
return session { env : bwSecretEnv ( appdata , creds , sess ) } , nil
}
2026-06-24 10:20:05 +00:00
type getOpts struct {
name string
field string
json bool
}
var validGetFields = map [ string ] bool { "password" : true , "username" : true , "uri" : true , "notes" : true , "totp" : true }
func parseGetArgs ( args [ ] string ) ( getOpts , error ) {
o := getOpts { field : "password" }
for i := 0 ; i < len ( args ) ; i ++ {
a := args [ i ]
switch {
case a == "--json" :
o . json = true
case a == "--field" && i + 1 < len ( args ) :
o . field = args [ i + 1 ]
i ++
case strings . HasPrefix ( a , "--field=" ) :
o . field = strings . TrimPrefix ( a , "--field=" )
case ! strings . HasPrefix ( a , "-" ) && o . name == "" :
o . name = a
}
}
if o . name == "" {
return o , fmt . Errorf ( "usage: homelab vault get <name> [--field password|username|uri|notes|totp] [--json]" )
}
if ! validGetFields [ o . field ] {
return o , fmt . Errorf ( "invalid --field %q (want password|username|uri|notes|totp)" , o . field )
}
return o , nil
}
// getValue opens a session and fetches one field. Pure of I/O side effects
// besides the runner, so it is unit-tested with a fake runner.
func getValue ( run cmdRunner , user , uid string , o getOpts ) ( string , error ) {
s , err := openSession ( run , user , uid )
if err != nil {
return "" , err
}
return bwGet ( run , s . env , o . field , o . name )
}
2026-06-24 10:28:31 +00:00
// clipboardDecision picks how to return a secret value. "stdout" prints it (a
// pipe/agent — the intended machine path); "clipboard" copies via OSC52;
// "refuse" emits nothing sensitive (would otherwise risk dumping the secret's
// base64 into scrollback, or silently fail because the OSC52 escape goes to a
// non-terminal stderr).
func clipboardDecision ( stdoutTTY , stderrTTY bool , term , termProgram string ) string {
if ! stdoutTTY {
return "stdout"
}
if terminalAllowed ( term , termProgram ) && stderrTTY {
return "clipboard"
}
return "refuse"
}
// jsonToStdoutOK reports whether `--json` may print the secret to stdout — only
// when stdout is NOT a terminal (i.e. piped to a machine consumer).
func jsonToStdoutOK ( stdoutTTY bool ) bool { return ! stdoutTTY }
// emitSecret returns a value TTY-aware (see clipboardDecision). Never prints the
// secret to a terminal's stdout/scrollback.
2026-06-24 10:20:05 +00:00
func emitSecret ( value string ) {
2026-06-24 10:28:31 +00:00
switch clipboardDecision ( stdoutIsTTY ( ) , stderrIsTTY ( ) , os . Getenv ( "TERM" ) , os . Getenv ( "TERM_PROGRAM" ) ) {
case "stdout" :
2026-06-24 10:20:05 +00:00
fmt . Println ( value )
2026-06-24 10:28:31 +00:00
case "clipboard" :
fmt . Fprint ( os . Stderr , osc52 ( value ) )
fmt . Fprintln ( os . Stderr , "copied to clipboard; clearing in 30s" )
clearClipboardAfter ( 30 )
default : // refuse
fmt . Fprintln ( os . Stderr , "refusing to print secret: this terminal can't do OSC52 clipboard safely; pipe the command (e.g. | cat) or use a supported terminal" )
2026-06-24 10:20:05 +00:00
}
}
// clearClipboardAfter spawns a detached background clear so the secret doesn't
// linger in the clipboard. Best-effort.
func clearClipboardAfter ( seconds int ) {
exec . Command ( "sh" , "-c" , fmt . Sprintf ( "sleep %d; printf '%s'" , seconds , osc52clear ( ) ) ) . Start ( )
}
2026-06-24 10:21:07 +00:00
// listNames extracts "name (id)" from `bw list items` JSON; never values.
func listNames ( jsonOut string ) [ ] string {
var items [ ] struct {
ID string ` json:"id" `
Name string ` json:"name" `
}
if err := json . Unmarshal ( [ ] byte ( jsonOut ) , & items ) ; err != nil {
return nil
}
out := make ( [ ] string , 0 , len ( items ) )
for _ , it := range items {
out = append ( out , fmt . Sprintf ( "%s (%s)" , it . Name , it . ID ) )
}
return out
}
2026-06-24 10:20:05 +00:00
2026-06-24 10:21:07 +00:00
func runList ( run cmdRunner , user , uid , search string ) ( [ ] string , error ) {
s , err := openSession ( run , user , uid )
if err != nil {
return nil , err
}
out , err := run ( "bw" , bwListArgs ( search ) , s . env )
if err != nil {
return nil , err
}
return listNames ( out ) , nil
}
func vaultList ( args [ ] string ) error {
hardenProcess ( )
search := ""
for i := 0 ; i < len ( args ) ; i ++ {
if args [ i ] == "--search" && i + 1 < len ( args ) {
search = args [ i + 1 ]
i ++
} else if strings . HasPrefix ( args [ i ] , "--search=" ) {
search = strings . TrimPrefix ( args [ i ] , "--search=" )
}
}
uid := vaultCurrentUID ( )
unlock , err := withUserLock ( uid )
if err != nil {
return err
}
defer unlock ( )
names , err := runList ( realRunner , vaultCurrentUser ( ) , uid , search )
if err != nil {
return err
}
for _ , n := range names {
fmt . Println ( n )
}
return nil
}
2026-06-24 10:20:05 +00:00
2026-06-24 10:21:07 +00:00
func vaultSearch ( args [ ] string ) error {
if len ( args ) == 0 {
return fmt . Errorf ( "usage: homelab vault search <query>" )
}
return vaultList ( [ ] string { "--search" , strings . Join ( args , " " ) } )
}
func vaultCode ( args [ ] string ) error {
hardenProcess ( )
if len ( args ) == 0 {
return fmt . Errorf ( "usage: homelab vault code <name>" )
}
name := args [ 0 ]
uid := vaultCurrentUID ( )
unlock , err := withUserLock ( uid )
if err != nil {
return err
}
defer unlock ( )
user := vaultCurrentUser ( )
val , err := getValue ( realRunner , user , uid , getOpts { name : name , field : "totp" } )
if err != nil {
return err
}
// TOTP is the most sensitive op: log AND emit an ntfy-bound marker (spec §9a-d).
writeOpLog ( opRecord { User : user , Verb : "code" , PID : os . Getpid ( ) , PPID : os . Getppid ( ) , ParentComm : parentComm ( os . Getppid ( ) ) , ItemName : name } )
exec . Command ( "logger" , "-t" , "homelab-vault-totp" , "user=" + user + " totp-fetch parent=" + parentComm ( os . Getppid ( ) ) ) . Run ( )
emitSecret ( val )
return nil
}
// statusSummary reports config/reachability without revealing secrets.
func statusSummary ( run cmdRunner , user , uid string ) string {
if _ , err := loadCreds ( run , user ) ; err != nil {
return "vault: not configured — run `homelab vault setup`"
}
s , err := openSession ( run , user , uid )
if err != nil {
return "vault: configured, but unlock/login FAILED (creds stale? run `homelab vault setup`): " + err . Error ( )
}
if _ , err := run ( "bw" , [ ] string { "sync" } , s . env ) ; err != nil {
return "vault: configured + unlocked, but sync/reachability failed: " + err . Error ( )
}
return "vault: configured, unlocked, reachable ✓"
}
func vaultStatus ( args [ ] string ) error {
hardenProcess ( )
uid := vaultCurrentUID ( )
unlock , err := withUserLock ( uid )
if err != nil {
return err
}
defer unlock ( )
fmt . Println ( statusSummary ( realRunner , vaultCurrentUser ( ) , uid ) )
return nil
}
func vaultLock ( args [ ] string ) error {
2026-06-24 10:28:31 +00:00
uid := vaultCurrentUID ( )
unlock , err := withUserLock ( uid ) // logout mutates bw state — serialize with get/list
if err != nil {
return err
}
defer unlock ( )
appdata := bwAppDataDir ( uid )
2026-06-24 10:21:07 +00:00
_ , _ = realRunner ( "bw" , [ ] string { "lock" } , bwBaseEnv ( appdata ) )
2026-06-24 10:28:31 +00:00
_ , logoutErr := realRunner ( "bw" , [ ] string { "logout" } , bwBaseEnv ( appdata ) )
if logoutErr == nil {
2026-06-24 10:21:07 +00:00
fmt . Println ( "locked" )
}
return nil // lock/logout best-effort; never error the caller
}
2026-06-24 10:28:31 +00:00
// vaultPatchPublicArgs writes the non-secret identifiers via argv. Neither the
// email nor the API client_id is a usable credential on its own.
func vaultPatchPublicArgs ( user , email , clientID string ) [ ] string {
2026-06-24 10:21:57 +00:00
return [ ] string { "kv" , "patch" , vwCredsPath ( user ) ,
2026-06-24 10:28:31 +00:00
"vaultwarden_email=" + email ,
"vaultwarden_client_id=" + clientID ,
2026-06-24 10:21:57 +00:00
}
}
2026-06-24 10:28:31 +00:00
// vaultPatchSecretArgs writes ONE secret value via the `key=-` stdin form, so
// the value never appears in argv (ps / /proc/<pid>/cmdline). The value is fed
// on stdin by realRunnerStdin.
func vaultPatchSecretArgs ( user , key string ) [ ] string {
return [ ] string { "kv" , "patch" , vwCredsPath ( user ) , key + "=-" }
}
// writeCreds stores all four fields in the user's Vault path. The two real
// secrets (master password, API client_secret) go via stdin — never argv.
func writeCreds ( user string , c vwCreds ) error {
if _ , err := realRunner ( "vault" , vaultPatchPublicArgs ( user , c . Email , c . ClientID ) , nil ) ; err != nil {
return err
}
if _ , err := realRunnerStdin ( "vault" , vaultPatchSecretArgs ( user , "vaultwarden_master_password" ) , nil , c . MasterPassword ) ; err != nil {
return err
}
if _ , err := realRunnerStdin ( "vault" , vaultPatchSecretArgs ( user , "vaultwarden_client_secret" ) , nil , c . ClientSecret ) ; err != nil {
return err
}
return nil
}
2026-06-24 10:21:57 +00:00
// promptNoEcho reads one line without terminal echo (for the master password).
func promptNoEcho ( prompt string ) ( string , error ) {
fmt . Fprint ( os . Stderr , prompt )
exec . Command ( "stty" , "-echo" ) . Run ( )
defer func ( ) { exec . Command ( "stty" , "echo" ) . Run ( ) ; fmt . Fprintln ( os . Stderr ) } ( )
r := bufio . NewReader ( os . Stdin )
line , err := r . ReadString ( '\n' )
2026-06-24 10:28:31 +00:00
// Trim only the line terminator — a master password / API secret may
// legitimately contain leading/trailing spaces.
return strings . TrimRight ( line , "\r\n" ) , err
2026-06-24 10:21:57 +00:00
}
func promptLine ( prompt string ) ( string , error ) {
fmt . Fprint ( os . Stderr , prompt )
line , err := bufio . NewReader ( os . Stdin ) . ReadString ( '\n' )
return strings . TrimSpace ( line ) , err
}
func vaultSetup ( args [ ] string ) error {
hardenProcess ( )
fmt . Fprintln ( os . Stderr , "One-time setup. Stored ONLY in your own Vault path; the admin never sees it." )
fmt . Fprintln ( os . Stderr , "Get your API key at https://vaultwarden.viktorbarzin.me → Settings → Security → Keys → View API key." )
email , err := promptLine ( "Vaultwarden email: " )
if err != nil {
return err
}
clientID , err := promptLine ( "API key client_id (user.xxxx): " )
if err != nil {
return err
}
clientSecret , err := promptNoEcho ( "API key client_secret: " )
if err != nil {
return err
}
master , err := promptNoEcho ( "Master password: " )
if err != nil {
return err
}
if master == "" || clientID == "" || clientSecret == "" {
return fmt . Errorf ( "all fields are required" )
}
c := vwCreds { Email : email , MasterPassword : master , ClientID : clientID , ClientSecret : clientSecret }
2026-06-24 10:28:31 +00:00
if err := writeCreds ( vaultCurrentUser ( ) , c ) ; err != nil {
2026-06-24 10:21:57 +00:00
return fmt . Errorf ( "writing creds to your Vault path failed (scoped token present?): %w" , err )
}
fmt . Fprintln ( os . Stderr , "Stored. Verifying unlock…" )
uid := vaultCurrentUID ( )
unlock , err := withUserLock ( uid )
if err != nil {
return err
}
defer unlock ( )
if _ , err := openSession ( realRunner , vaultCurrentUser ( ) , uid ) ; err != nil {
return fmt . Errorf ( "stored, but verification failed — double-check master password / API key: %w" , err )
}
fmt . Fprintln ( os . Stderr , "✓ Verified. Fetches are now AFK." )
return nil
}
2026-06-24 10:20:05 +00:00
func vaultGet ( args [ ] string ) error {
hardenProcess ( )
o , err := parseGetArgs ( args )
if err != nil {
return err
}
uid := vaultCurrentUID ( )
unlock , err := withUserLock ( uid )
if err != nil {
return err
}
defer unlock ( )
user := vaultCurrentUser ( )
val , err := getValue ( realRunner , user , uid , o )
if err != nil {
return err
}
writeOpLog ( opRecord { User : user , Verb : "get" , PID : os . Getpid ( ) , PPID : os . Getppid ( ) , ParentComm : parentComm ( os . Getppid ( ) ) , ItemName : o . name } )
if o . json {
2026-06-24 10:28:31 +00:00
if ! jsonToStdoutOK ( stdoutIsTTY ( ) ) {
return fmt . Errorf ( "refusing to print a secret as JSON to a terminal; pipe it (e.g. | cat) or drop --json" )
}
2026-06-24 10:20:05 +00:00
fmt . Printf ( "{%q:%q}\n" , o . field , val )
return nil
}
emitSecret ( val )
return nil
}