localvault 1.8.1 → 1.9.0

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: f49d7a925b997780d6d03a2b071117e20da0827d24331d28c9cd5043a70b0056
4
+ data.tar.gz: 27b2be6fa5bb90d3df68eec4a27fcba483a175b574afd96f7aeba7dd0e175412
5
5
  SHA512:
6
- metadata.gz: 2e4409c50f9aa0350b86541e905703ed24520cd583498d49de11a320cda8b6c2ebf86bc14c377bed52cd83438ba2254b0e243d91ffcf5521f7f84d9445072e1c
7
- data.tar.gz: 97ff4555610dc166e82d01b6f11200ce516d0d36acb701625519c73b6e2f31d9096ae53e19d863128246d240d54eb82ebbd6f60ed51309ef4a8ec45675b144fd
6
+ metadata.gz: 7863c066d383db0a2ad4a98ae9665fe1fafc04de61b1fdbfec590d4af0ea04e47f31f1635a4c63d5886f0f71780a5f9f0ebd143348af23d404415991487dd214
7
+ data.tar.gz: b61cb7b5096ad2250560a1462f87ecfd81e538ed823971c46fe4820be3bb50443574055e40d0e080c7f4e5f7b7ce93d2d2f9cceedd03ea4f39a9a13141f35c44
data/README.md CHANGED
@@ -145,6 +145,9 @@ backward compatibility but the top-level forms are preferred.
145
145
  | `install-mcp [CLIENT]` | Configure MCP server in claude-code, cursor, or windsurf |
146
146
  | `mcp` | Start MCP server (stdio transport) |
147
147
  | `doctor` | Check install and PATH readiness, including brew/asdf shadowing |
148
+ | `guard install` | Install Claude Code hooks that block secrets in agent commands (v1.9.0) |
149
+ | `guard status` | Show guard hook installation state |
150
+ | `guard hook` | Hook entrypoint (reads hook JSON on stdin; not run by hand) |
148
151
 
149
152
  All commands accept `--vault NAME` (or `-v NAME`) to target a specific vault. Default vault is `default`.
150
153
 
@@ -310,6 +313,39 @@ Unlocking writes a derived key to `LOCALVAULT_SESSION` and also caches it with a
310
313
 
311
314
  ## Security
312
315
 
316
+ ### Agent Plaintext Containment (v1.9.0)
317
+
318
+ Two layers keep secrets out of AI agent context and transcripts:
319
+
320
+ **1. Plaintext refuses captured streams.** `get`, `env`, and `show --reveal`
321
+ print values to an interactive terminal as always. When stdout is captured
322
+ (a pipe or an agent's shell), a human confirms with one keypress on `/dev/tty`;
323
+ an agent's shell has no `/dev/tty`, so it is refused and pointed at injection:
324
+
325
+ ```bash
326
+ localvault get STRIPE_KEY # human at a terminal: prints
327
+ localvault get STRIPE_KEY | pbcopy # human piping: "Print plaintext? [y/N]"
328
+ # agent shell: refused → use localvault exec --map STRIPE_KEY=STRIPE_KEY -- CMD
329
+ ```
330
+
331
+ There is deliberately no flag or environment variable to bypass this — the only
332
+ override is a keypress on a real terminal. Headless automation uses
333
+ `localvault exec` injection.
334
+
335
+ **2. Guard hooks block secrets in agent commands.** `localvault guard install`
336
+ wires Claude Code hooks that scan every Bash tool call against your unlocked
337
+ vaults. A command containing a stored secret value is blocked before it runs,
338
+ naming the key by fingerprint — never by value:
339
+
340
+ ```text
341
+ LocalVault guard: blocked — this tool input contains the plaintext value of
342
+ default/STRIPE.private_key (sha256:1a2b3c4d5e6f). Inject it instead:
343
+ localvault exec --map STRIPE.private_key=PRIVATE_KEY -- your-command
344
+ ```
345
+
346
+ The installed hook fails open: locked vaults, an old binary, or a missing
347
+ install allow the call rather than breaking your session.
348
+
313
349
  ### Crypto Stack
314
350
 
315
351
  | Layer | Algorithm | Purpose |
@@ -0,0 +1,87 @@
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
+ help_output = `localvault help 2>/dev/null`
68
+ return if help_output.include?("guard")
69
+
70
+ $stderr.puts "Note: the `localvault` on your PATH does not support `guard hook` " \
71
+ "(old version or different install). The hook fails open and guards " \
72
+ "nothing until you upgrade: brew upgrade localvault"
73
+ rescue StandardError
74
+ nil
75
+ end
76
+
77
+ def settings_path(project)
78
+ if project
79
+ File.join(Dir.pwd, ".claude", "settings.json")
80
+ else
81
+ File.join(Dir.home, ".claude", "settings.json")
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ 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"
@@ -292,9 +293,9 @@ module LocalVault
292
293
  lookup = KeyLookup.lookup(vault, key)
293
294
 
294
295
  if lookup.exact?
295
- $stdout.puts lookup.value
296
+ print_plaintext(key, lookup.value)
296
297
  elsif lookup.single_match?
297
- $stdout.puts vault.get(lookup.matches.first)
298
+ print_plaintext(lookup.matches.first, vault.get(lookup.matches.first))
298
299
  elsif lookup.multiple_matches?
299
300
  $stderr.puts "Error: Multiple keys match '#{key}'. Be more specific:"
300
301
  lookup.matches.each { |k| $stderr.puts " #{k}" }
@@ -403,6 +404,15 @@ module LocalVault
403
404
  method_option :profile, type: :string, desc: "Apply a built-in env mapping profile (aws)"
404
405
  def env
405
406
  vault = open_vault!
407
+ unless PlaintextOutput.permitted?(purpose: "Export plaintext values")
408
+ abort_with <<~MSG.strip
409
+ refusing to print plaintext env exports: stdout is a captured stream, not an interactive terminal.
410
+ Use process-scoped injection instead:
411
+ localvault exec [--only KEYS|--map KEY=ENV_NAME|--profile aws] -- your-command
412
+ A human at a terminal is asked to confirm; agents and CI must use injection.
413
+ MSG
414
+ return
415
+ end
406
416
  skip_warn = ->(k) { $stderr.puts "Warning: skipping unsafe key '#{k}'" }
407
417
  $stdout.puts vault.export_env(**env_projection_options(on_skip: skip_warn))
408
418
  rescue EnvProjection::InvalidMapping, EnvProjection::UnknownProfile => e
@@ -520,6 +530,7 @@ module LocalVault
520
530
  def show
521
531
  vault = open_vault!
522
532
  secrets = vault.all
533
+ reveal = options[:reveal] && reveal_permitted?
523
534
 
524
535
  named_group_query = options[:group] && ![GROUP_ALL_SENTINEL, GROUP_OFF_SENTINEL].include?(options[:group])
525
536
  if secrets.empty? && !named_group_query
@@ -533,21 +544,21 @@ module LocalVault
533
544
  abort_with "No project '#{options[:project]}' in vault '#{vault.name}'"
534
545
  return
535
546
  end
536
- render_table(group.sort.to_h, "#{vault.name}/#{options[:project]}", reveal: options[:reveal])
547
+ render_table(group.sort.to_h, "#{vault.name}/#{options[:project]}", reveal: reveal)
537
548
  elsif options[:group] && ![GROUP_ALL_SENTINEL, GROUP_OFF_SENTINEL].include?(options[:group])
538
549
  match = GroupCatalog.new(secrets).resolve(options[:group])
539
550
  if match.group
540
551
  entries = match.group.entries.to_h { |entry| [entry.label, entry.value] }
541
- render_table(entries, "#{vault.name}/#{match.group.name}", reveal: options[:reveal])
552
+ render_table(entries, "#{vault.name}/#{match.group.name}", reveal: reveal)
542
553
  elsif match.kind == :ambiguous
543
554
  raise GroupSelectionError.new(:ambiguous, query: options[:group], candidates: match.groups.map(&:name))
544
555
  else
545
556
  raise GroupSelectionError.new(:absent, query: options[:group])
546
557
  end
547
558
  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])
559
+ render_grouped_table(secrets, vault.name, reveal: reveal)
549
560
  else
550
- render_table(secrets.sort.to_h, vault.name, reveal: options[:reveal])
561
+ render_table(secrets.sort.to_h, vault.name, reveal: reveal)
551
562
  end
552
563
  end
553
564
 
@@ -754,7 +765,9 @@ module LocalVault
754
765
  require_relative "cli/keys"
755
766
  require_relative "cli/team"
756
767
  require_relative "cli/sync"
768
+ require_relative "cli/guard"
757
769
 
770
+ register(Guard, "guard", "guard SUBCOMMAND", "Block plaintext secrets in agent tool traffic (Claude Code hooks)")
758
771
  register(Keys, "keys", "keys SUBCOMMAND", "Manage your X25519 keypair for vault sharing")
759
772
  register(Team, "team", "team SUBCOMMAND", "Manage vault team access")
760
773
  register(Sync, "sync", "sync SUBCOMMAND", "Sync vaults to InventList cloud")
@@ -1915,6 +1928,29 @@ module LocalVault
1915
1928
  $stderr.puts "Error: #{message}"
1916
1929
  end
1917
1930
 
1931
+ # --- plaintext gating (see docs/plans/07-agent-plaintext-containment.md) ---
1932
+
1933
+ def print_plaintext(key, value)
1934
+ unless PlaintextOutput.permitted?(purpose: "Print plaintext value of '#{key}'")
1935
+ env_name = key.split(".").last.upcase
1936
+ abort_with <<~MSG.strip
1937
+ refusing to print plaintext for '#{key}': stdout is a captured stream, not an interactive terminal.
1938
+ Use process-scoped injection instead:
1939
+ localvault exec --map #{key}=#{env_name} -- your-command
1940
+ A human at a terminal is asked to confirm; agents and CI must use injection.
1941
+ MSG
1942
+ return
1943
+ end
1944
+ $stdout.puts value
1945
+ end
1946
+
1947
+ def reveal_permitted?
1948
+ return true if PlaintextOutput.permitted?(purpose: "Reveal plaintext values")
1949
+
1950
+ $stderr.puts "Masking values: stdout is a captured stream. Use `localvault exec` for injection."
1951
+ false
1952
+ end
1953
+
1918
1954
  # --- install-mcp helpers ---
1919
1955
 
1920
1956
  # 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.0"
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.0
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