localvault 1.8.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 86be5d86ef86c74bcfc777561088264d1b908eaca92a2b501d5e23d937e5a906
4
- data.tar.gz: d3d079c65ff54ca198907957ae3193a3a73520aa84169cf0067bc254967fea05
3
+ metadata.gz: a7cdca7447f0efeb125773a3385a6e3beee17cb03d4c05e04b91dc59254215dd
4
+ data.tar.gz: b3378c85ad59e00561d5eb5f590e203469d86f8c88925e7b547721717175f5a4
5
5
  SHA512:
6
- metadata.gz: f0d5b4b8d1575b8b91fbd742afd0aef2f71401e72882149cf0b83e68cd233636592c5e0af85512007b4cff604d466ff38d5ac085ea7eea60e9b9446a3d7247a5
7
- data.tar.gz: 98a51ac12f4857e0f381fa4b4449a63687d402fe3bf3d97e693f75bfc526440efbc0fd9df14f397c99771c4c871c9a8f164b51e29cca56a232bfa78c099ca8c9
6
+ metadata.gz: 2e4409c50f9aa0350b86541e905703ed24520cd583498d49de11a320cda8b6c2ebf86bc14c377bed52cd83438ba2254b0e243d91ffcf5521f7f84d9445072e1c
7
+ data.tar.gz: 97ff4555610dc166e82d01b6f11200ce516d0d36acb701625519c73b6e2f31d9096ae53e19d863128246d240d54eb82ebbd6f60ed51309ef4a8ec45675b144fd
data/README.md CHANGED
@@ -41,10 +41,10 @@ sudo dnf install libsodium-devel
41
41
  # Create a vault (prompts for passphrase)
42
42
  localvault init
43
43
 
44
- # Store secrets
45
- localvault set OPENAI_API_KEY "sk-proj-..."
46
- localvault set STRIPE_SECRET_KEY "sk_live_..."
47
- localvault set DATABASE_URL "postgres://localhost/myapp"
44
+ # Store secrets without putting values in shell history / process args
45
+ printf '%s' "$OPENAI_API_KEY" | localvault set OPENAI_API_KEY --stdin
46
+ printf '%s' "$STRIPE_SECRET_KEY" | localvault set STRIPE_SECRET_KEY --stdin
47
+ printf '%s' "$DATABASE_URL" | localvault set DATABASE_URL --stdin
48
48
 
49
49
  # Retrieve a secret (raw, pipeable)
50
50
  localvault get OPENAI_API_KEY
@@ -69,8 +69,9 @@ localvault exec -- rails server
69
69
  | Command | Description |
70
70
  |---------|-------------|
71
71
  | `init [NAME]` | Create a vault (Argon2id key derivation) |
72
- | `set KEY VALUE` | Store a secret (supports dot-notation: `project.KEY`) |
73
- | `set --group GROUP KEY VALUE` | Store a secret in a named group |
72
+ | `set KEY --stdin` | Store a secret from stdin without argv/history exposure |
73
+ | `set KEY [VALUE]` | Store a secret (positional value kept for compatibility) |
74
+ | `set --group GROUP KEY --stdin` | Store a secret in a named group from stdin |
74
75
  | `get KEY` | Retrieve a secret (raw, pipeable) |
75
76
  | `show` | Display all secrets in a table (masked by default) |
76
77
  | `show --reveal` | Display with values visible |
@@ -143,6 +144,7 @@ backward compatibility but the top-level forms are preferred.
143
144
  |---------|-------------|
144
145
  | `install-mcp [CLIENT]` | Configure MCP server in claude-code, cursor, or windsurf |
145
146
  | `mcp` | Start MCP server (stdio transport) |
147
+ | `doctor` | Check install and PATH readiness, including brew/asdf shadowing |
146
148
 
147
149
  All commands accept `--vault NAME` (or `-v NAME`) to target a specific vault. Default vault is `default`.
148
150
 
@@ -232,6 +234,9 @@ localvault unlock
232
234
  # Verify setup without starting the blocking stdio server
233
235
  localvault mcp --check
234
236
 
237
+ # Diagnose brew/asdf PATH shadowing after upgrades
238
+ localvault doctor
239
+
235
240
  # MCP tools available to the agent:
236
241
  # localvault_whoami — diagnose active vault/session state
237
242
  # list_secrets(vault?, prefix?, query?) — list/search key names
@@ -60,8 +60,12 @@ module LocalVault
60
60
  when :ambiguous then "Error: group name is ambiguous. Existing groups: #{@error.candidates.join(", ")}."
61
61
  else "Error: group and key names may contain letters, digits, and underscores only."
62
62
  end
63
+ elsif @error.is_a?(CLI::SetValueSourceError) && @error.kind == :multiple
64
+ "Error: #{@error.message}"
63
65
  elsif group_save_attempt?
64
66
  "Error: saving in a group needs GROUP, KEY, and VALUE."
67
+ elsif @error.is_a?(CLI::SetValueSourceError)
68
+ "Error: #{@error.message}"
65
69
  elsif unknown_option
66
70
  "Error: unknown option `#{unknown_option}`."
67
71
  elsif context.command.nil?
@@ -97,7 +101,7 @@ module LocalVault
97
101
  end
98
102
 
99
103
  return group_save_suggestions if group_save_attempt?
100
- return ["localvault set KEY VALUE", *group_save_suggestions] if context.command&.name == "set"
104
+ return set_suggestions if context.command&.name == "set"
101
105
  return show_group_suggestions if context.command&.name == "show" || unknown_option == "--group-by"
102
106
  return ["localvault add HANDLE", "localvault team add HANDLE"] if context.namespace == ["team"] && context.command&.name == "add"
103
107
 
@@ -117,6 +121,14 @@ module LocalVault
117
121
  ]
118
122
  end
119
123
 
124
+ def set_suggestions
125
+ [
126
+ %(printf '%s' "$SECRET" | localvault set KEY --stdin),
127
+ "localvault set KEY VALUE",
128
+ *group_save_suggestions
129
+ ]
130
+ end
131
+
120
132
  def show_group_suggestions
121
133
  [
122
134
  "localvault show --group GROUP",
@@ -5,6 +5,7 @@ require "lipgloss"
5
5
  require_relative "env_projection"
6
6
  require_relative "key_lookup"
7
7
  require_relative "session_cache"
8
+ require_relative "stdin_secret_input"
8
9
  require_relative "vault_resolver"
9
10
  require_relative "group_catalog"
10
11
 
@@ -49,6 +50,18 @@ module LocalVault
49
50
  1
50
51
  end
51
52
  end
53
+ class SetValueSourceError < Thor::Error
54
+ attr_reader :kind
55
+
56
+ def initialize(kind, message)
57
+ @kind = kind
58
+ super(message)
59
+ end
60
+
61
+ def exit_status
62
+ 1
63
+ end
64
+ end
52
65
 
53
66
  def self.start(given_args = ARGV, config = {})
54
67
  require_relative "cli/error_presenter"
@@ -105,7 +118,9 @@ module LocalVault
105
118
  shell.say " localvault demo Create a demo vault to explore commands"
106
119
  shell.say ""
107
120
  shell.say "SECRETS"
108
- shell.say " localvault set KEY VALUE Store a secret"
121
+ shell.say " printf '%s' \"$SECRET\" | localvault set KEY --stdin"
122
+ shell.say " Store a secret without argv/history exposure"
123
+ shell.say " localvault set KEY VALUE Store a secret (compatibility path)"
109
124
  shell.say " localvault set --group G K V Store a secret inside group G"
110
125
  shell.say " localvault get KEY Retrieve a secret"
111
126
  shell.say " localvault show Display all secrets (masked by default)"
@@ -169,6 +184,7 @@ module LocalVault
169
184
  shell.say " localvault login --status Show current login status"
170
185
  shell.say " localvault logout Log out"
171
186
  shell.say " localvault version Print version"
187
+ shell.say " localvault doctor Check install and PATH readiness"
172
188
  shell.say " localvault help [COMMAND] Full help for any command"
173
189
  shell.say ""
174
190
  end
@@ -197,10 +213,14 @@ module LocalVault
197
213
  abort_with e.message
198
214
  end
199
215
 
200
- desc "set KEY VALUE", "Store a secret (supports dot-notation for nested keys)"
216
+ desc "set KEY [VALUE]", "Store a secret (supports dot-notation for nested keys)"
201
217
  long_desc <<~DESC
202
218
  Store a secret in the current vault.
203
219
 
220
+ SAFE INPUT (preferred):
221
+ \x05 printf '%s' "$SECRET" | localvault set KEY --stdin
222
+ \x05 printf '%s' "$SECRET" | localvault set --group GROUP KEY --stdin
223
+
204
224
  FLAT KEY (simple):
205
225
  \x05 localvault set DATABASE_URL postgres://localhost/myapp
206
226
  \x05 localvault set STRIPE_KEY sk_live_abc123
@@ -215,33 +235,42 @@ module LocalVault
215
235
  \x05 localvault set GROUP.KEY VALUE
216
236
  \x05 localvault groups [QUERY]
217
237
 
238
+ Positional values may be visible in process lists and shell history.
218
239
  The dot separates project from key name. One vault can hold many projects.
219
240
  Use `localvault show -p platepose -v vault` to view a single project.
220
241
  Use `localvault import` to bulk-load from a .env, .json, or .yml file.
221
242
  DESC
222
243
  method_option :group, type: :string, desc: "Store KEY and VALUE inside this named group"
223
- def set(key, value)
244
+ method_option :stdin, type: :boolean, default: false, desc: "Read VALUE from stdin instead of argv"
245
+ def set(key, value = nil)
246
+ validate_secret_value_source!(value)
224
247
  vault = open_vault!
225
248
  if options[:group]
226
249
  group = canonical_group_name(vault, options[:group])
227
250
  validate_group_segment!(group)
228
251
  validate_group_segment!(key)
229
252
  raise GroupSaveError, :collision if vault.all.key?(group) && !vault.all[group].is_a?(Hash)
253
+ value = read_secret_value(value)
230
254
  vault.set("#{group}.#{key}", value)
231
255
  $stdout.puts "Set #{key} in group `#{group}` in vault `#{vault.name}`."
232
256
  $stdout.puts
233
257
  $stdout.puts "Stored as:"
234
258
  $stdout.puts " #{group}.#{key}"
235
259
  else
260
+ value = read_secret_value(value)
236
261
  vault.set(key, value)
237
262
  $stdout.puts "Set #{key} in vault '#{vault.name}'"
238
263
  end
264
+ rescue StdinSecretInput::InteractiveInput, StdinSecretInput::InvalidEncoding => e
265
+ raise SetValueSourceError.new(:stdin, e.message)
239
266
  rescue Vault::InvalidKeyName => e
240
267
  raise GroupSaveError, :invalid if options[:group]
241
268
  abort_with e.message
269
+ CommandStatus.error
242
270
  rescue RuntimeError => e
243
271
  raise GroupSaveError, :collision if options[:group]
244
272
  abort_with e.message
273
+ CommandStatus.error
245
274
  end
246
275
 
247
276
  desc "get KEY", "Retrieve a secret value by key"
@@ -1463,6 +1492,46 @@ module LocalVault
1463
1492
  $stdout.puts "localvault #{VERSION}"
1464
1493
  end
1465
1494
 
1495
+ desc "doctor", "Check install and PATH readiness"
1496
+ long_desc <<~DESC
1497
+ Check whether the localvault executable selected by PATH matches the
1498
+ install you expect.
1499
+
1500
+ This catches common Homebrew/asdf shadowing issues after upgrades:
1501
+ \x05 localvault doctor
1502
+ \x05 which -a localvault
1503
+ DESC
1504
+ def doctor
1505
+ paths = localvault_paths
1506
+ warnings = localvault_path_warnings(paths)
1507
+
1508
+ $stdout.puts "LocalVault doctor"
1509
+ $stdout.puts "Version: localvault #{VERSION}"
1510
+ $stdout.puts "Home: #{Config.root_path}"
1511
+
1512
+ if paths.empty?
1513
+ $stdout.puts "Executable selected by PATH: not found"
1514
+ else
1515
+ $stdout.puts "Executable selected by PATH: #{paths.first}"
1516
+ $stdout.puts "All localvault executables on PATH:"
1517
+ paths.each_with_index { |path, index| $stdout.puts " #{index + 1}. #{path}" }
1518
+ end
1519
+
1520
+ if warnings.empty?
1521
+ $stdout.puts "PATH: ok"
1522
+ CommandStatus.ok
1523
+ else
1524
+ $stdout.puts
1525
+ warnings.each { |warning| $stdout.puts "Warning: #{warning}" }
1526
+ $stdout.puts
1527
+ $stdout.puts "Suggested checks:"
1528
+ $stdout.puts " asdf reshim ruby"
1529
+ $stdout.puts " hash -r"
1530
+ $stdout.puts " which -a localvault"
1531
+ CommandStatus.error
1532
+ end
1533
+ end
1534
+
1466
1535
  def self.exit_on_failure?
1467
1536
  false
1468
1537
  end
@@ -1489,6 +1558,22 @@ module LocalVault
1489
1558
 
1490
1559
  private
1491
1560
 
1561
+ def validate_secret_value_source!(value)
1562
+ if options[:stdin] && !value.nil?
1563
+ raise SetValueSourceError.new(:multiple, "Use either a positional VALUE or --stdin, not both.")
1564
+ end
1565
+
1566
+ return if options[:stdin] || !value.nil?
1567
+
1568
+ raise SetValueSourceError.new(:missing, "Provide a VALUE or read one with --stdin.")
1569
+ end
1570
+
1571
+ def read_secret_value(value)
1572
+ return value unless options[:stdin]
1573
+
1574
+ StdinSecretInput.read($stdin)
1575
+ end
1576
+
1492
1577
  def canonical_group_name(vault, supplied)
1493
1578
  groups = GroupCatalog.new(vault.all).groups
1494
1579
  return supplied if groups.any? { |group| group.name == supplied }
@@ -1903,14 +1988,47 @@ module LocalVault
1903
1988
 
1904
1989
  no_commands do
1905
1990
  def find_binary(name)
1906
- path = `which #{name} 2>/dev/null`.strip
1907
- path.empty? ? nil : path
1991
+ executable_paths_for(name).first
1908
1992
  end
1909
1993
 
1910
1994
  def system_command_exists?(cmd)
1911
1995
  !find_binary(cmd).nil?
1912
1996
  end
1913
1997
 
1998
+ def localvault_paths
1999
+ executable_paths_for("localvault")
2000
+ end
2001
+
2002
+ def executable_paths_for(name)
2003
+ ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).filter_map do |directory|
2004
+ next if directory.empty?
2005
+
2006
+ path = File.join(directory, name)
2007
+ path if File.executable?(path) && !File.directory?(path)
2008
+ end.uniq
2009
+ end
2010
+
2011
+ def localvault_path_warnings(paths)
2012
+ warnings = []
2013
+ if paths.empty?
2014
+ warnings << "localvault is not on PATH. Brew upgrades may be installed but unreachable."
2015
+ return warnings
2016
+ end
2017
+
2018
+ if paths.first.include?("/.asdf/shims/") && paths.any? { |path| homebrew_localvault_path?(path) }
2019
+ warnings << "PATH selects an asdf shim before Homebrew localvault. " \
2020
+ "A stale shim can hide the upgraded brew executable."
2021
+ elsif paths.length > 1
2022
+ warnings << "Multiple localvault executables are on PATH. Confirm the first entry is the one you intend."
2023
+ end
2024
+
2025
+ warnings
2026
+ end
2027
+
2028
+ def homebrew_localvault_path?(path)
2029
+ path.start_with?("/opt/homebrew/bin/", "/usr/local/bin/")
2030
+ end
2031
+
1914
2032
  def cursor_settings_path
1915
2033
  File.expand_path("~/.cursor/mcp.json")
1916
2034
  end
@@ -0,0 +1,22 @@
1
+ module LocalVault
2
+ class StdinSecretInput
3
+ class InteractiveInput < StandardError; end
4
+ class InvalidEncoding < StandardError; end
5
+
6
+ PIPE_EXAMPLE = %(printf '%s' "$SECRET" | localvault set KEY --stdin).freeze
7
+
8
+ def self.read(input = $stdin)
9
+ if input.respond_to?(:tty?) && input.tty?
10
+ raise InteractiveInput, "Refusing to read secret from interactive stdin. " \
11
+ "Pipe it instead: #{PIPE_EXAMPLE}"
12
+ end
13
+
14
+ input.binmode if input.respond_to?(:binmode)
15
+ value = input.read || ""
16
+ value = value.dup.force_encoding(Encoding::UTF_8)
17
+ raise InvalidEncoding, "Secret value must be valid UTF-8" unless value.valid_encoding?
18
+
19
+ value
20
+ end
21
+ end
22
+ end
@@ -1,3 +1,3 @@
1
1
  module LocalVault
2
- VERSION = "1.8.0"
2
+ VERSION = "1.8.1"
3
3
  end
data/lib/localvault.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "localvault/store"
5
5
  require_relative "localvault/env_projection"
6
6
  require_relative "localvault/key_lookup"
7
7
  require_relative "localvault/vault_resolver"
8
+ require_relative "localvault/stdin_secret_input"
8
9
  require_relative "localvault/vault"
9
10
  require_relative "localvault/identity"
10
11
  require_relative "localvault/share_crypto"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: localvault
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.0
4
+ version: 1.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nauman Tariq
@@ -126,6 +126,7 @@ files:
126
126
  - lib/localvault/mcp/tools.rb
127
127
  - lib/localvault/session_cache.rb
128
128
  - lib/localvault/share_crypto.rb
129
+ - lib/localvault/stdin_secret_input.rb
129
130
  - lib/localvault/store.rb
130
131
  - lib/localvault/sync_bundle.rb
131
132
  - lib/localvault/sync_state.rb