Skip to main content
Glama

vaultshell

English | 简体中文

CI npm License: MIT

Available on the official MCP Registry: io.github.SoWhatI/vaultshell

An MCP server that stores secrets in local secure storage, injects them into shell child processes at exec time based on rules, and guarantees secret plaintext never reaches the model context — not in tool responses, not in logs, not in audit records.

The closed loop: reference-based storage + conditional injection + mandatory output redaction.

Status: M3 — one-shot commands + persistent sessions + credential proxy + external CLI backends. See CHANGELOG and docs/design-spec.md.

The iron rule

  • No MCP tool returns secret plaintext — ever. Responses contain only names, masks ([REDACTED:NAME]), and redacted output.

  • Resolvers are only invoked inside the launcher; values go straight into the child process env and never pass through the tool layer.

  • shell_exec has no free-form env parameter. Secrets enter by rule reference only.

  • Redactor failures are fail-closed: output is discarded, never returned raw.

Related MCP server: host-shell-mcp

Quick start

Requires Node.js 20+.

Install from GitHub (no npm registry account needed)

# Run on demand directly from the repo (npm installs devDependencies and
# builds dist/ via the `prepare` script):
npx -y github:SoWhatI/vaultshell

# Or install globally:
npm i -g github:SoWhatI/vaultshell

This is the same code as the npm registry package, installed from git — npm clones the repo, runs prepare (→ npm run build) and links the vaultshell bin. Pin a tag for reproducibility: npx -y github:SoWhatI/vaultshell#v0.1.1.

Docker (ghcr.io)

docker run -i --rm \
  -e MASTER_KEY=<64 hex chars> \
  -v ~/.vaultshell:/home/node/.vaultshell \
  ghcr.io/sowhati/vaultshell:latest

MCP client config using Docker:

{
  "mcpServers": {
    "vaultshell": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "MASTER_KEY=<64 hex chars>",
        "-v", "~/.vaultshell:/home/node/.vaultshell",
        "ghcr.io/sowhati/vaultshell:latest"
      ]
    }
  }
}

The image runs as the non-root node user (HOME=/home/node), so the data volume mounts at /home/node/.vaultshell. node-pty is excluded from the image — sessions automatically fall back to pipe mode (pty: false) in containers; injection and redaction are unchanged. The web subcommand passes through (docker run … ghcr.io/sowhati/vaultshell web — loopback inside the container, of limited use).

No Smithery distribution: Smithery has gone hosted-only (it only accepts servers deployed to its own infrastructure). vaultshell's secrets must stay on the user's machine by design — third-party hosting is incompatible with its threat model, so we deliberately do not publish there.

From source

# Run via npx (after publish) or from source:
npm install && npm run build

# 1. Create a master key for the encrypted-file backend
export MASTER_KEY=$(openssl rand -hex 32)

# 2. Create ~/.vaultshell/config.yaml and ~/.vaultshell/rules.yaml
#    (full annotated examples: docs/user-guide.md)

# 3. Store a secret (never echoed back) and run with injection
#    via your MCP client's tools: secret_set, then shell_exec

Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "vaultshell": {
      "command": "npx",
      "args": ["-y", "vaultshell"],
      "env": { "MASTER_KEY": "<64 hex chars>" }
    }
  }
}

Full setup, configuration reference, rule-writing guide, client integration, and FAQ: docs/user-guide.md.

MCP tools

Tool

Purpose

Returns

shell_exec

Run a one-shot command with per-rule injection. Params: command, cwd?, profile?, extraEnv? (allowlist), dryRun?, timeoutSeconds? (≤3600)

{exitCode, stdout, stderr} (redacted), injected:[names], ruleId, redactedCount, timedOut, truncated, warnings; dryRun → {dryRun:true, ruleId, injected, envKeys, denied}

shell_session_open

Open a persistent session; secrets injected once at creation

{sessionId, cwd, injected:[names], pty, ttlSeconds}

shell_session_send

Send a command to a session

{output} (merged, redacted), exitCode, redactedCount

shell_session_close

Kill a session; secrets die with the process

{ok}

shell_session_list

List live sessions

metadata only

shell_session_revoke

Kill and rebuild the session without secrets

{sessionId} (new)

shell_proxy_start

Start a credential-injecting reverse proxy on 127.0.0.1

{proxyId, port, expiresAt, upstreamHost}

shell_proxy_stop

Stop a proxy

{ok}

shell_proxy_list

List running proxies

metadata only (incl. secret name, never value)

secret_list

List names + metadata (backend ref, resolvable)

never values

secret_set

Write to backend; persisted, never echoed

{name, ok}

secret_delete

Delete and unregister

{name, ok}

secret_probe

Check a ref resolves

{ok, masked:"****"}

rule_list

Rules + static warnings (e.g. inject-everywhere)

—

rule_validate

Static findings: unknown secrets, unreachable rules, inject-everywhere, union risk, requireConfirm capability

{ok, findings[]}

audit_query

Read audit entries

names + redacted commands only

Execution limits (M3)

  • dryRun: true on shell_exec reports the would-be ruleId, injected names, final env key list (never values) and the deny-list verdict — without executing anything.

  • timeoutSeconds (default defaults.execTimeoutSeconds = 120, capped at 3600) kills the command and returns timedOut: true.

  • defaults.maxOutputBytes (default 1 MiB) truncates each output stream and sets truncated: true. Truncation happens after the Redactor — it can never bypass redaction.

  • defaults.maxConcurrentExecs (default 8) rejects excess concurrent shell_exec calls with a clear error (no queue).

  • All of the above are recorded in the audit log (dryRun / timedOut / truncated flags).

Web config UI

vaultshell web            # loopback-only HTTP UI on a random port
vaultshell web --port 5317

Prints a one-time-token URL like http://127.0.0.1:5317/?token=… — open it to manage secrets (write-only), edit rules with live static-validation warnings and a matcher dry-run, edit config, and browse the redacted audit log. The token dies with the process; every API call needs it, mutations require same-origin application/json requests, and no endpoint can ever return a secret value. Do not port-forward it. Design & threat model: docs/web-config-ui-design.md.

Credential proxy (M3)

For high-sensitivity API tokens, prefer not landing the secret in env at all. Declare a proxy in config.yaml:

proxies:
  - id: stripe
    upstreamHost: https://api.stripe.com
    secretRef: STRIPE_KEY                          # registry name or full ref
    headerTemplate: "Authorization: Bearer ${value}"

Then shell_proxy_start { "id": "stripe" } → { port, expiresAt }, and commands call http://127.0.0.1:<port>/v1/charges instead of https://api.stripe.com/v1/charges. The proxy injects the header in memory only and auto-stops after its TTL (default 300s). Audit records the host and secret name, never the header value.

Boundaries: http:///https:// upstreams only (proxy→upstream TLS is properly validated; no CONNECT tunneling, no TLS termination); the client→proxy leg is plaintext on 127.0.0.1, which inherits the same-UID local-process boundary from the threat model.

Persistent sessions (M2)

shell_session_open spawns a long-lived shell with the matched rule's secrets injected at creation. The session is recycled after an idle TTL (defaults.sessionTtlSeconds, default 900s, overridable per rule via ttlSeconds or per call) — when the process dies, the secrets die with it. shell_session_revoke kills a possibly-compromised session immediately and rebuilds one without any secrets.

Sessions use a real PTY via node-pty (an optional dependency — native build, needs Xcode CLT on macOS / build-essential + python3 on Linux). If node-pty cannot be loaded or spawned, vaultshell automatically falls back to a plain child_process shell and the shell_session_open response says pty: false; injection and redaction behave identically, only interactivity is degraded. Session output merges stdout/stderr (PTY semantics) and passes through the same Redactor.

Backend support matrix

Backend

Ref scheme

Status

Platforms

Notes

Encrypted file

encfile://NAME

✅ Implemented

all

AES-256-GCM; master key from env:MASTER_KEY or macOS keychain; default

Local keychain

keychain://svc/acct

⚠️ macOS only

macOS

spawns security CLI; Linux/Windows give a clear error

Process env

env://NAME

✅ Implemented

all

read-only; good for CI

File

file://path

✅ Implemented

all

first line; refuses permissions > 0600

Inline

inline:

⚠️ Restricted

all

plaintext in config; disabled by default, startup warning when enabled

1Password

op://vault/item/field

✅ via CLI

all

needs authenticated op CLI; read-only

Vault

vault://path#field

✅ via CLI

all

needs authenticated vault CLI; read-only

Infisical

infisical://proj/env/KEY

✅ via CLI

all

needs authenticated infisical CLI; read-only

Doppler

doppler://proj/config/KEY

✅ via CLI

all

needs authenticated doppler CLI; read-only

Details, per-backend setup, and how to register a plugin resolver: docs/backends.md.

Threat model

Threat

Mitigation

Model-context leakage

Structural: values never cross the tool layer; only names and masks

Command output echoing secrets

Mandatory redaction (exact + URL/Base64 variants + generic patterns), fail-closed

Agent dumping env (env, printenv, /proc/*/environ, …)

Hard-blocked deny-list (configurable via security.dangerousCommands: extra patterns, or mode: warn to allow-with-audit) + shell_exec_blocked audit events

Same-UID local process reading /proc/PID/environ

OS-level limit, cannot be fully fixed; per-command injection shrinks the window. Explicit boundary.

Long secret residency

Per-command injection by default; sessions have idle-TTL recycling + instant revoke

Misconfigured rule causing inject-everywhere

rule_validate / rule_list static warnings; mergeStrategy defaults to override; requireConfirm rules demand interactive approval (fail-closed without elicitation)

Config files leaking

Config stores refs only, never values; inline: disabled by default; store files forced to 0600

Full discussion: docs/design-spec.md §7. Report vulnerabilities privately per SECURITY.md.

Data layout

~/.vaultshell/
  config.yaml     # main config
  rules.yaml      # injection rules + secret refs (refs only, never values)
  secrets.enc     # encrypted-file backend (0600)
  audit/          # JSONL audit, redacted

VAULTSHELL_HOME overrides the data directory (used by tests).

Development

npm run build   # tsc
npm test        # vitest — redactor property tests, backend round-trips,
                # rule matching, shell_exec end-to-end

See CONTRIBUTING.md — including the security red lines every PR must respect.

License

MIT © 2026 vaultshell contributors

Available Tools

16 tools
audit_queryB

Query recent audit entries (newest last). Entries contain names and redacted commands only, never values.

Inputs: limit.

rule_listA

List injection rules with static warnings (e.g. rules matching every directory → potential unintended full injection).

Inputs: no named parameters.

rule_validateA

Statically validate rules.yaml and return findings: unknown secret refs, unreachable rules (shadowed by earlier ones), inject-everywhere rules (high severity), requireConfirm capability notes, union mergeStrategy risk notes.

Inputs: no named parameters.

secret_deleteB

Delete a secret from the backend and unregister it from rules.yaml.

Inputs: name (required).

secret_listA

List registered secret names with metadata (backend ref, resolvable). Never returns values.

Inputs: no named parameters.

secret_probeB

Check whether a secret ref can be resolved. Returns only {ok, masked}; masked never contains value fragments unless defaults.maskTail > 0.

Inputs: name (required).

secret_setB

Store a secret value into the configured backend. The value is persisted, never echoed back, and the name is registered in rules.yaml (ref only).

Inputs: name (required), value (required), ref.

shell_execA

Execute a one-shot shell command with secrets injected per matching rule. Output is redacted: injected secret values never appear in the response. Dangerous commands (env, printenv, export -p, /proc/*/environ, ...) are hard-blocked. There is no free-form env parameter; secrets are only injected by reference via rules.

Inputs: command (required), cwd, profile, extraEnv.

shell_proxy_listA

List running credential proxies (metadata only: id, port, upstreamHost, secret NAME, expiry, request count). Never includes values.

Inputs: no named parameters.

shell_proxy_startA

Start a credential-injecting reverse proxy on 127.0.0.1 (random port) for a proxies entry in config.yaml. Point commands at http://127.0.0.1: instead of the real API host; the proxy injects the secret into the configured header in memory only. Auto-stops after its TTL (default defaults.proxyTtlSeconds=300s). The secret value is never returned anywhere.

Inputs: id (required), ttlSeconds.

shell_proxy_stopC

Stop a running credential proxy.

Inputs: proxyId (required).

shell_session_closeB

Close a session: kill the process; injected secrets die with it.

Inputs: sessionId (required).

shell_session_listA

List live sessions (metadata only: id, cwd, injected names, pty, ttl, timestamps).

Inputs: no named parameters.

shell_session_openA

Open a persistent shell session (PTY when node-pty is available, plain child_process otherwise — the response's pty field says which). Secrets are injected once at creation per matching rule; idle sessions are killed after ttlSeconds (rule ttlSeconds overrides defaults.sessionTtlSeconds).

Inputs: cwd, profile, ttlSeconds.

shell_session_revokeA

Immediately kill a session and rebuild a fresh one WITHOUT any injected secrets. Use when a session may have been compromised. Returns the new sessionId.

Inputs: sessionId (required).

shell_session_sendA

Send a command to a persistent session. Output (merged stdout/stderr) is redacted before returning. Dangerous commands are blocked per security.dangerousCommands. Returns {output, exitCode}.

Inputs: command (required), sessionId (required).

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.7/5.0

    Scored across 16 tools

    Disambiguation4/5

    Most tools target distinct resources and operations, covering secret lifecycle, shell execution, persistent sessions, proxy management, rules, and audit. The only slight overlap is between secret_probe and secret_list, since both can report secret resolvability, but their descriptions clarify the intended use.

    Naming Consistency5/5

    All tool names use snake_case with clear domain prefixes such as secret_, shell_, shell_session_, shell_proxy_, rule_, and audit_. The verb_noun structure is consistent throughout, with no mixed conventions or vague naming.

    Tool Count4/5

    The server has 16 tools across five coherent subdomains, slightly above the typical 3-15 range but still well-scoped. Each tool appears to earn its place, with no obvious redundant operations.

    Completeness4/5

    Core secret, shell, session, proxy, and audit workflows are well covered, including session revocation and proxy lifecycle. Rule management is read-only via list and validate, with no create/update/delete tools, which is a minor gap likely handled through the underlying rules.yaml file.

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables AI agents to securely use secrets by running commands with environment-injected credentials and sanitizing output to prevent leakage.
      1,011 npm
      1
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables AI agents to execute shell commands on a host macOS computer with configurable file access permissions, granting access to credentials and tools while enforcing sandbox restrictions.
      MIT
    • A
      license
      Not graded
      quality
      Not graded
      maintenance
      Enables autonomous agents to run shell and subprocess commands through a guardrail that blocks destructive system mutations, path traversals, and reverse shells. It also adds prompt-injection detection, spend circuit breakers, loop detection, and privacy sanitization so agent execution stays within safe, budgeted bounds.
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables Claude Code to use locally encrypted, multi-tenant credentials by running commands with injected secrets, writing .env files, materializing files, and making HTTP requests, while scrubbing values from all output so they never enter the chat. Also supports sealed retrieval and saving through native dialogs or an auto-clearing clipboard, role-based credential selection, versioning, rollback, archiving, and a full audit log.
      9 npm
      MIT