localvault 1.8.1 → 1.9.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: a7cdca7447f0efeb125773a3385a6e3beee17cb03d4c05e04b91dc59254215dd
4
- data.tar.gz: b3378c85ad59e00561d5eb5f590e203469d86f8c88925e7b547721717175f5a4
3
+ metadata.gz: 20aa094af163dfa0173fae2eabfc274da78badf84ebc37366bf85163f7b7f77d
4
+ data.tar.gz: f113382f8edb4a630732b56de9abe163470c0c377bbcbf64951fde108f6d5e0b
5
5
  SHA512:
6
- metadata.gz: 2e4409c50f9aa0350b86541e905703ed24520cd583498d49de11a320cda8b6c2ebf86bc14c377bed52cd83438ba2254b0e243d91ffcf5521f7f84d9445072e1c
7
- data.tar.gz: 97ff4555610dc166e82d01b6f11200ce516d0d36acb701625519c73b6e2f31d9096ae53e19d863128246d240d54eb82ebbd6f60ed51309ef4a8ec45675b144fd
6
+ metadata.gz: e65ac37ab002da71e42ed5d1e31369dd492982683e2817165060962777e197b8873b6c73c11761d2f7aa829c1c6729f7334691d83d16f0de858c59de2a8abbec
7
+ data.tar.gz: 2efaf71acba0f24beaa48313a88d0a9f58058d42f7c80a5ef439af76f29deac6df30435dd74868d04d1bbedf224e1d87499a647e32509cecf0895d696f453934
data/README.md CHANGED
@@ -16,12 +16,29 @@ Part of [InventList Tools](https://inventlist.com/tools/localvault) — free, op
16
16
  brew install inventlist/tap/localvault
17
17
  ```
18
18
 
19
+ ### Install script (no Homebrew)
20
+
21
+ ```bash
22
+ curl -sSL https://raw.githubusercontent.com/inventlist/localvault/main/install.sh | sh
23
+ ```
24
+
25
+ Installs into an isolated prefix (`~/.localvault/runtime`) and writes a wrapper
26
+ to your PATH that pins its own Ruby. It never adds a version-manager shim and
27
+ never touches your project gems, so `localvault` keeps working when you switch
28
+ Ruby versions. Override with `LOCALVAULT_BIN_DIR`, `LOCALVAULT_PREFIX`,
29
+ `LOCALVAULT_VERSION`, or `LOCALVAULT_RUBY`.
30
+
19
31
  ### RubyGems
20
32
 
21
33
  ```bash
22
34
  gem install localvault
23
35
  ```
24
36
 
37
+ A plain `gem install` under asdf/rbenv creates a shim that can shadow your
38
+ Homebrew build and silently pin you to an old version. If `localvault version`
39
+ disagrees with what you installed, run `localvault doctor` — it lists every
40
+ copy on your PATH.
41
+
25
42
  **Requires libsodium:**
26
43
 
27
44
  ```bash
@@ -145,6 +162,9 @@ backward compatibility but the top-level forms are preferred.
145
162
  | `install-mcp [CLIENT]` | Configure MCP server in claude-code, cursor, or windsurf |
146
163
  | `mcp` | Start MCP server (stdio transport) |
147
164
  | `doctor` | Check install and PATH readiness, including brew/asdf shadowing |
165
+ | `guard install` | Install Claude Code hooks that block secrets in agent commands (v1.9.0) |
166
+ | `guard status` | Show guard hook installation state |
167
+ | `guard hook` | Hook entrypoint (reads hook JSON on stdin; not run by hand) |
148
168
 
149
169
  All commands accept `--vault NAME` (or `-v NAME`) to target a specific vault. Default vault is `default`.
150
170
 
@@ -310,6 +330,39 @@ Unlocking writes a derived key to `LOCALVAULT_SESSION` and also caches it with a
310
330
 
311
331
  ## Security
312
332
 
333
+ ### Agent Plaintext Containment (v1.9.0)
334
+
335
+ Two layers keep secrets out of AI agent context and transcripts:
336
+
337
+ **1. Plaintext refuses captured streams.** `get`, `env`, and `show --reveal`
338
+ print values to an interactive terminal as always. When stdout is captured
339
+ (a pipe or an agent's shell), a human confirms with one keypress on `/dev/tty`;
340
+ an agent's shell has no `/dev/tty`, so it is refused and pointed at injection:
341
+
342
+ ```bash
343
+ localvault get STRIPE_KEY # human at a terminal: prints
344
+ localvault get STRIPE_KEY | pbcopy # human piping: "Print plaintext? [y/N]"
345
+ # agent shell: refused → use localvault exec --map STRIPE_KEY=STRIPE_KEY -- CMD
346
+ ```
347
+
348
+ There is deliberately no flag or environment variable to bypass this — the only
349
+ override is a keypress on a real terminal. Headless automation uses
350
+ `localvault exec` injection.
351
+
352
+ **2. Guard hooks block secrets in agent commands.** `localvault guard install`
353
+ wires Claude Code hooks that scan every Bash tool call against your unlocked
354
+ vaults. A command containing a stored secret value is blocked before it runs,
355
+ naming the key by fingerprint — never by value:
356
+
357
+ ```text
358
+ LocalVault guard: blocked — this tool input contains the plaintext value of
359
+ default/STRIPE.private_key (sha256:1a2b3c4d5e6f). Inject it instead:
360
+ localvault exec --map STRIPE.private_key=PRIVATE_KEY -- your-command
361
+ ```
362
+
363
+ The installed hook fails open: locked vaults, an old binary, or a missing
364
+ install allow the call rather than breaking your session.
365
+
313
366
  ### Crypto Stack
314
367
 
315
368
  | Layer | Algorithm | Purpose |
@@ -0,0 +1,88 @@
1
+ require "thor"
2
+ require "json"
3
+ require "fileutils"
4
+ require_relative "../guard"
5
+
6
+ module LocalVault
7
+ class CLI
8
+ class Guard < Thor
9
+ desc "hook", "Claude Code hook entrypoint (reads hook JSON on stdin)"
10
+ long_desc <<~DESC
11
+ Reads a Claude Code PreToolUse/PostToolUse event on stdin. Blocks tool
12
+ calls whose input contains a stored plaintext secret value from any
13
+ session-unlocked vault, naming the key by fingerprint — never by value.
14
+ Fails open on any error so a broken guard cannot block all work.
15
+ DESC
16
+ def hook
17
+ event = JSON.parse($stdin.read)
18
+ result = LocalVault::Guard.evaluate(event)
19
+ if result[:exit] != 0
20
+ $stderr.puts result[:message]
21
+ exit result[:exit]
22
+ end
23
+ rescue StandardError
24
+ # fail open
25
+ end
26
+
27
+ desc "install", "Install the guard hooks into Claude Code settings"
28
+ method_option :project, type: :boolean, default: false,
29
+ desc: "Install into ./.claude/settings.json instead of ~/.claude/settings.json"
30
+ def install
31
+ path = settings_path(options[:project])
32
+ settings = File.exist?(path) ? JSON.parse(File.read(path)) : {}
33
+ if LocalVault::Guard.merge_hooks!(settings)
34
+ FileUtils.mkdir_p(File.dirname(path))
35
+ File.write(path, JSON.pretty_generate(settings) + "\n")
36
+ $stdout.puts "Installed LocalVault guard hooks in #{path}"
37
+ $stdout.puts "Restart Claude Code sessions to pick up hook changes."
38
+ else
39
+ $stdout.puts "LocalVault guard hooks already installed in #{path}"
40
+ end
41
+ warn_if_path_binary_lacks_guard
42
+ rescue JSON::ParserError
43
+ $stderr.puts "Error: #{path} is not valid JSON; fix it before installing."
44
+ end
45
+
46
+ desc "status", "Show guard hook installation status"
47
+ def status
48
+ { "user" => settings_path(false), "project" => settings_path(true) }.each do |label, path|
49
+ state = if !File.exist?(path)
50
+ "not installed"
51
+ else
52
+ begin
53
+ LocalVault::Guard.installed?(JSON.parse(File.read(path))) ? "installed" : "not installed"
54
+ rescue JSON::ParserError
55
+ "unreadable JSON"
56
+ end
57
+ end
58
+ $stdout.puts "#{label} (#{path}): #{state}"
59
+ end
60
+ end
61
+
62
+ no_commands do
63
+ # The installed hook wraps the entrypoint to fail open, so an old or
64
+ # missing PATH binary never breaks the user's sessions — but it also
65
+ # silently guards nothing, which deserves a loud note at install time.
66
+ def warn_if_path_binary_lacks_guard
67
+ # Probe the PATH binary itself: `guard status` exits 0 only where the
68
+ # command exists. Parsing curated help text is not a version check.
69
+ return if system("localvault guard status", out: File::NULL, err: File::NULL)
70
+
71
+ $stderr.puts "Note: the `localvault` on your PATH does not support `guard hook` " \
72
+ "(old version or different install). The hook fails open and guards " \
73
+ "nothing until you upgrade: brew upgrade localvault"
74
+ rescue StandardError
75
+ nil
76
+ end
77
+
78
+ def settings_path(project)
79
+ if project
80
+ File.join(Dir.pwd, ".claude", "settings.json")
81
+ else
82
+ File.join(Dir.home, ".claude", "settings.json")
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -4,6 +4,7 @@ require "base64"
4
4
  require "lipgloss"
5
5
  require_relative "env_projection"
6
6
  require_relative "key_lookup"
7
+ require_relative "plaintext_output"
7
8
  require_relative "session_cache"
8
9
  require_relative "stdin_secret_input"
9
10
  require_relative "vault_resolver"
@@ -173,6 +174,8 @@ module LocalVault
173
174
  shell.say " localvault mcp Start MCP server (stdio)"
174
175
  shell.say " localvault mcp --check Check setup and active-vault readiness"
175
176
  shell.say " Agents: list names, then use localvault_build_exec for safe injection"
177
+ shell.say " localvault guard install Block secrets in agent commands (Claude Code hooks)"
178
+ shell.say " localvault guard status Show guard hook installation state"
176
179
  shell.say ""
177
180
  shell.say "LEGACY SHARING (pre-v1.2 direct share, still works as fallback)"
178
181
  shell.say " localvault keygen Generate X25519 keypair (same as `keys generate`)"
@@ -292,9 +295,9 @@ module LocalVault
292
295
  lookup = KeyLookup.lookup(vault, key)
293
296
 
294
297
  if lookup.exact?
295
- $stdout.puts lookup.value
298
+ print_plaintext(key, lookup.value)
296
299
  elsif lookup.single_match?
297
- $stdout.puts vault.get(lookup.matches.first)
300
+ print_plaintext(lookup.matches.first, vault.get(lookup.matches.first))
298
301
  elsif lookup.multiple_matches?
299
302
  $stderr.puts "Error: Multiple keys match '#{key}'. Be more specific:"
300
303
  lookup.matches.each { |k| $stderr.puts " #{k}" }
@@ -403,6 +406,15 @@ module LocalVault
403
406
  method_option :profile, type: :string, desc: "Apply a built-in env mapping profile (aws)"
404
407
  def env
405
408
  vault = open_vault!
409
+ unless PlaintextOutput.permitted?(purpose: "Export plaintext values")
410
+ abort_with <<~MSG.strip
411
+ refusing to print plaintext env exports: stdout is a captured stream, not an interactive terminal.
412
+ Use process-scoped injection instead:
413
+ localvault exec [--only KEYS|--map KEY=ENV_NAME|--profile aws] -- your-command
414
+ A human at a terminal is asked to confirm; agents and CI must use injection.
415
+ MSG
416
+ return
417
+ end
406
418
  skip_warn = ->(k) { $stderr.puts "Warning: skipping unsafe key '#{k}'" }
407
419
  $stdout.puts vault.export_env(**env_projection_options(on_skip: skip_warn))
408
420
  rescue EnvProjection::InvalidMapping, EnvProjection::UnknownProfile => e
@@ -520,6 +532,7 @@ module LocalVault
520
532
  def show
521
533
  vault = open_vault!
522
534
  secrets = vault.all
535
+ reveal = options[:reveal] && reveal_permitted?
523
536
 
524
537
  named_group_query = options[:group] && ![GROUP_ALL_SENTINEL, GROUP_OFF_SENTINEL].include?(options[:group])
525
538
  if secrets.empty? && !named_group_query
@@ -533,21 +546,21 @@ module LocalVault
533
546
  abort_with "No project '#{options[:project]}' in vault '#{vault.name}'"
534
547
  return
535
548
  end
536
- render_table(group.sort.to_h, "#{vault.name}/#{options[:project]}", reveal: options[:reveal])
549
+ render_table(group.sort.to_h, "#{vault.name}/#{options[:project]}", reveal: reveal)
537
550
  elsif options[:group] && ![GROUP_ALL_SENTINEL, GROUP_OFF_SENTINEL].include?(options[:group])
538
551
  match = GroupCatalog.new(secrets).resolve(options[:group])
539
552
  if match.group
540
553
  entries = match.group.entries.to_h { |entry| [entry.label, entry.value] }
541
- render_table(entries, "#{vault.name}/#{match.group.name}", reveal: options[:reveal])
554
+ render_table(entries, "#{vault.name}/#{match.group.name}", reveal: reveal)
542
555
  elsif match.kind == :ambiguous
543
556
  raise GroupSelectionError.new(:ambiguous, query: options[:group], candidates: match.groups.map(&:name))
544
557
  else
545
558
  raise GroupSelectionError.new(:absent, query: options[:group])
546
559
  end
547
560
  elsif options[:group] != GROUP_OFF_SENTINEL && (options[:group] || secrets.values.any? { |v| v.is_a?(Hash) })
548
- render_grouped_table(secrets, vault.name, reveal: options[:reveal])
561
+ render_grouped_table(secrets, vault.name, reveal: reveal)
549
562
  else
550
- render_table(secrets.sort.to_h, vault.name, reveal: options[:reveal])
563
+ render_table(secrets.sort.to_h, vault.name, reveal: reveal)
551
564
  end
552
565
  end
553
566
 
@@ -754,7 +767,9 @@ module LocalVault
754
767
  require_relative "cli/keys"
755
768
  require_relative "cli/team"
756
769
  require_relative "cli/sync"
770
+ require_relative "cli/guard"
757
771
 
772
+ register(Guard, "guard", "guard SUBCOMMAND", "Block plaintext secrets in agent tool traffic (Claude Code hooks)")
758
773
  register(Keys, "keys", "keys SUBCOMMAND", "Manage your X25519 keypair for vault sharing")
759
774
  register(Team, "team", "team SUBCOMMAND", "Manage vault team access")
760
775
  register(Sync, "sync", "sync SUBCOMMAND", "Sync vaults to InventList cloud")
@@ -1915,6 +1930,29 @@ module LocalVault
1915
1930
  $stderr.puts "Error: #{message}"
1916
1931
  end
1917
1932
 
1933
+ # --- plaintext gating (see docs/plans/07-agent-plaintext-containment.md) ---
1934
+
1935
+ def print_plaintext(key, value)
1936
+ unless PlaintextOutput.permitted?(purpose: "Print plaintext value of '#{key}'")
1937
+ env_name = key.split(".").last.upcase
1938
+ abort_with <<~MSG.strip
1939
+ refusing to print plaintext for '#{key}': stdout is a captured stream, not an interactive terminal.
1940
+ Use process-scoped injection instead:
1941
+ localvault exec --map #{key}=#{env_name} -- your-command
1942
+ A human at a terminal is asked to confirm; agents and CI must use injection.
1943
+ MSG
1944
+ return
1945
+ end
1946
+ $stdout.puts value
1947
+ end
1948
+
1949
+ def reveal_permitted?
1950
+ return true if PlaintextOutput.permitted?(purpose: "Reveal plaintext values")
1951
+
1952
+ $stderr.puts "Masking values: stdout is a captured stream. Use `localvault exec` for injection."
1953
+ false
1954
+ end
1955
+
1918
1956
  # --- install-mcp helpers ---
1919
1957
 
1920
1958
  # Claude Code: use `claude mcp add --scope user` so the server is
@@ -0,0 +1,148 @@
1
+ require "json"
2
+ require "digest"
3
+ require_relative "store"
4
+ require_relative "session_cache"
5
+
6
+ module LocalVault
7
+ # Scans agent tool traffic (Claude Code hook events) for stored plaintext
8
+ # secret values, so a value already in an agent's context — retrieved or
9
+ # freshly generated, once stored — can never pass through a command line
10
+ # unnoticed.
11
+ #
12
+ # Failure posture is fail-open: locked vaults, unreadable stores, and
13
+ # malformed events all allow the tool call. A locked vault cannot have fed
14
+ # values into the session, and a guard that blocks all work when it cannot
15
+ # check gets uninstalled.
16
+ module Guard
17
+ MIN_VALUE_LENGTH = 8
18
+ HOOK_ENTRYPOINT = "localvault guard hook".freeze
19
+ # The installed command must fail open on machines where the binary is
20
+ # old, missing, or broken — otherwise every Bash call errors for users
21
+ # whose settings outlive their localvault install. Only a genuine deny
22
+ # (exit 2) is allowed through; every other exit becomes a silent allow.
23
+ HOOK_COMMAND = %(sh -c 'out=$(#{HOOK_ENTRYPOINT} 2>&1); s=$?; if [ $s -eq 2 ]; then echo "$out" >&2; exit 2; fi; exit 0').freeze
24
+ HOOK_EVENTS = %w[PreToolUse PostToolUse].freeze
25
+ ALLOW = { exit: 0, message: nil }.freeze
26
+
27
+ Match = Struct.new(:vault, :key, :fingerprint, keyword_init: true)
28
+
29
+ # Plaintext values from every session-unlocked vault.
30
+ #
31
+ # @return [Array<Hash>] entries with :vault, :key, :value
32
+ def self.unlocked_secrets
33
+ Store.list_vaults.flat_map do |name|
34
+ master_key = SessionCache.get(name)
35
+ next [] unless master_key
36
+
37
+ begin
38
+ vault = Vault.new(name: name, master_key: master_key)
39
+ flatten(vault.all).map { |key, value| { vault: name, key: key, value: value } }
40
+ rescue StandardError
41
+ []
42
+ end
43
+ end
44
+ end
45
+
46
+ def self.flatten(hash, prefix = nil)
47
+ hash.each_with_object({}) do |(k, v), out|
48
+ key = prefix ? "#{prefix}.#{k}" : k.to_s
49
+ if v.is_a?(Hash)
50
+ out.merge!(flatten(v, key))
51
+ else
52
+ out[key] = v.to_s
53
+ end
54
+ end
55
+ end
56
+
57
+ # @return [Array<Match>] stored secret values appearing in +text+
58
+ def self.scan(text, secrets = unlocked_secrets)
59
+ return [] if text.nil? || text.empty?
60
+
61
+ secrets.filter_map do |entry|
62
+ value = entry[:value]
63
+ next if value.nil? || value.length < MIN_VALUE_LENGTH
64
+ next unless text.include?(value)
65
+
66
+ Match.new(vault: entry[:vault], key: entry[:key], fingerprint: fingerprint(value))
67
+ end
68
+ end
69
+
70
+ def self.fingerprint(value)
71
+ Digest::SHA256.hexdigest(value)[0, 12]
72
+ end
73
+
74
+ def self.strings_in(node)
75
+ case node
76
+ when String then [node]
77
+ when Hash then node.values.flat_map { |v| strings_in(v) }
78
+ when Array then node.flat_map { |v| strings_in(v) }
79
+ else []
80
+ end
81
+ end
82
+
83
+ # Evaluate a parsed Claude Code hook event.
84
+ #
85
+ # @return [Hash] +{exit: Integer, message: String|nil}+ — exit 2 blocks a
86
+ # PreToolUse call / surfaces a PostToolUse warning to the agent
87
+ def self.evaluate(event, secrets = unlocked_secrets)
88
+ case event["hook_event_name"]
89
+ when "PreToolUse"
90
+ matches = scan(strings_in(event["tool_input"]).join("\n"), secrets)
91
+ matches.empty? ? ALLOW : { exit: 2, message: deny_message(matches) }
92
+ when "PostToolUse"
93
+ matches = scan(strings_in(event["tool_response"]).join("\n"), secrets)
94
+ matches.empty? ? ALLOW : { exit: 2, message: exposure_message(matches) }
95
+ else
96
+ ALLOW
97
+ end
98
+ rescue StandardError
99
+ ALLOW
100
+ end
101
+
102
+ def self.deny_message(matches)
103
+ first = matches.first
104
+ env_name = first.key.split(".").last.upcase
105
+ <<~MSG.strip
106
+ LocalVault guard: blocked — this tool input contains the plaintext value of #{name_list(matches)}.
107
+ Never place secret values in commands or arguments. Inject them instead:
108
+ localvault exec --map #{first.key}=#{env_name} -- your-command
109
+ or pipe a new value with: printf '%s' "$VALUE" | localvault set KEY --stdin
110
+ MSG
111
+ end
112
+
113
+ def self.exposure_message(matches)
114
+ <<~MSG.strip
115
+ LocalVault guard: this command's output contained the plaintext value of #{name_list(matches)} and has entered the transcript.
116
+ Treat the value as exposed: rotate it, then store the replacement via --stdin.
117
+ Avoid commands that print secrets; use scoped injection (localvault exec --only/--map).
118
+ MSG
119
+ end
120
+
121
+ def self.name_list(matches)
122
+ matches.map { |m| "#{m.vault}/#{m.key} (sha256:#{m.fingerprint})" }.join(", ")
123
+ end
124
+
125
+ # Idempotently add the guard hook entries to a Claude Code settings hash.
126
+ #
127
+ # @return [Boolean] whether the settings were modified
128
+ def self.merge_hooks!(settings)
129
+ changed = false
130
+ hooks = settings["hooks"] ||= {}
131
+ HOOK_EVENTS.each do |event|
132
+ entries = hooks[event] ||= []
133
+ next if entries.any? { |e| (e["hooks"] || []).any? { |h| h["command"].to_s.include?(HOOK_ENTRYPOINT) } }
134
+
135
+ entries << { "matcher" => "Bash", "hooks" => [{ "type" => "command", "command" => HOOK_COMMAND }] }
136
+ changed = true
137
+ end
138
+ changed
139
+ end
140
+
141
+ def self.installed?(settings)
142
+ hooks = settings["hooks"] || {}
143
+ HOOK_EVENTS.all? do |event|
144
+ (hooks[event] || []).any? { |e| (e["hooks"] || []).any? { |h| h["command"].to_s.include?(HOOK_ENTRYPOINT) } }
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,48 @@
1
+ module LocalVault
2
+ # Gate for printing plaintext secret values.
3
+ #
4
+ # Plaintext may flow to an interactive terminal (a human is watching). When
5
+ # stdout is captured (pipe, command substitution, agent shell), a human can
6
+ # confirm with one keystroke on /dev/tty. An agent's shell has no /dev/tty,
7
+ # and there is deliberately no flag, environment variable, or config bypass:
8
+ # anything discoverable from the binary would be discovered by an agent.
9
+ module PlaintextOutput
10
+ TTY_PATH = "/dev/tty".freeze
11
+
12
+ class << self
13
+ # In-process overrides for tests only — unreachable from the installed
14
+ # binary, unlike an env var or CLI flag would be.
15
+ attr_accessor :assume_tty
16
+ attr_accessor :tty_override # {input: IO, output: IO}
17
+ end
18
+
19
+ # @param out [IO] the stream plaintext would be written to
20
+ # @param purpose [String] short description used in the /dev/tty prompt
21
+ # @return [Boolean] whether plaintext may be printed to +out+
22
+ def self.permitted?(out: $stdout, purpose: "Print plaintext")
23
+ return true if assume_tty
24
+ return true if out.respond_to?(:tty?) && out.tty?
25
+
26
+ confirm("#{purpose}? [y/N] ")
27
+ end
28
+
29
+ def self.confirm(prompt)
30
+ with_tty do |input, output|
31
+ output.write(prompt)
32
+ output.flush
33
+ answer = input.gets
34
+ !answer.nil? && %w[y yes].include?(answer.strip.downcase)
35
+ end
36
+ end
37
+
38
+ def self.with_tty(&block)
39
+ if tty_override
40
+ return yield(tty_override[:input], tty_override[:output])
41
+ end
42
+
43
+ File.open(TTY_PATH, "r+") { |tty| yield(tty, tty) }
44
+ rescue SystemCallError, IOError
45
+ false
46
+ end
47
+ end
48
+ end
@@ -1,3 +1,3 @@
1
1
  module LocalVault
2
- VERSION = "1.8.1"
2
+ VERSION = "1.9.1"
3
3
  end
data/lib/localvault.rb CHANGED
@@ -6,6 +6,8 @@ require_relative "localvault/env_projection"
6
6
  require_relative "localvault/key_lookup"
7
7
  require_relative "localvault/vault_resolver"
8
8
  require_relative "localvault/stdin_secret_input"
9
+ require_relative "localvault/plaintext_output"
10
+ require_relative "localvault/guard"
9
11
  require_relative "localvault/vault"
10
12
  require_relative "localvault/identity"
11
13
  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.1
4
+ version: 1.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nauman Tariq
@@ -109,6 +109,7 @@ files:
109
109
  - lib/localvault/api_client.rb
110
110
  - lib/localvault/cli.rb
111
111
  - lib/localvault/cli/error_presenter.rb
112
+ - lib/localvault/cli/guard.rb
112
113
  - lib/localvault/cli/keys.rb
113
114
  - lib/localvault/cli/sync.rb
114
115
  - lib/localvault/cli/team.rb
@@ -117,6 +118,7 @@ files:
117
118
  - lib/localvault/crypto.rb
118
119
  - lib/localvault/env_projection.rb
119
120
  - lib/localvault/group_catalog.rb
121
+ - lib/localvault/guard.rb
120
122
  - lib/localvault/identity.rb
121
123
  - lib/localvault/input_validation.rb
122
124
  - lib/localvault/key_lookup.rb
@@ -124,6 +126,7 @@ files:
124
126
  - lib/localvault/mcp/exec_command_builder.rb
125
127
  - lib/localvault/mcp/server.rb
126
128
  - lib/localvault/mcp/tools.rb
129
+ - lib/localvault/plaintext_output.rb
127
130
  - lib/localvault/session_cache.rb
128
131
  - lib/localvault/share_crypto.rb
129
132
  - lib/localvault/stdin_secret_input.rb