@openparachute/vault
v0.3.3
Published
Agent-native knowledge graph. Notes, tags, links over MCP.
Downloads
1,246
Readme
Parachute Vault
Parachute Vault is a self-hosted knowledge graph that any AI can read and write, over the open MCP protocol. Your notes, tags, links, and attachments live on your machine — in plain SQLite databases under ~/.parachute/, not in a vendor's cloud.
Today it works with Claude Code, Codex, Goose, OpenCode, and any other local MCP client — same endpoint, your vault. Claude Code auto-wires on install; for the rest, point them at http://127.0.0.1:1940/vault/default/mcp.
Web AI connectors — claude.ai, ChatGPT, and Gemini — are coming in the next few weeks. Switch tools, keep your knowledge. No vendor lock-in, no re-import step when the next model lands.
Quick start
Requires Bun (curl -fsSL https://bun.sh/install | bash) and macOS 13+ or Linux. No root needed — see Requirements below for specifics.
# Install globally (registers the `parachute` CLI)
bun add -g @openparachute/vault
parachute-vault init
# Or clone and run directly
git clone https://github.com/ParachuteComputer/parachute-vault
cd parachute-vault
bun install
bun src/cli.ts vault initvault init creates a vault, generates an API key, starts a background daemon (launchd on Mac, systemd on Linux), and configures Claude Code's MCP — all in one command. Start a new Claude Code session and your vault's tools show up. For other local MCP clients (Codex, Goose, OpenCode, Cursor, Zed, Cline, your own agent), point them at http://127.0.0.1:1940/vault/default/mcp — the API key is printed once at init; save it for anything that isn't Claude Code.
For remote access from Claude Desktop or mobile apps, see Deployment below.
What you get
A server on port 1940 with:
- MCP — 9 tools for AI agents (notes, tags, graph, vault info)
- REST API — Full CRUD for notes, tags, links, full-text search
- Wikilink auto-linking —
[[wikilinks]]in note content automatically create links in the graph - Obsidian import/export — Bidirectional interop with Obsidian vaults
- Webhook triggers — Config-driven webhooks that fire on note mutations matching tag/metadata predicates
- View endpoint — Serve notes as clean HTML pages (public or authenticated)
Each vault is its own SQLite database. Run multiple vaults on one server.
What vault init does
A mental model for "where is my data?" and "what can I poke at?" after the one-command setup.
On disk — ~/.parachute/
~/.parachute/ # ecosystem root — shared with sibling services
services.json # CLI-owned manifest (used by the dispatcher for
# discovery + routing across Parachute services)
well-known/ # CLI-owned (.well-known serving)
vault/ # everything vault owns
config.yaml # global config — port, default_vault, owner password
# hash, TOTP secret, backup-codes hashes, backup
# schedule. 0600.
.env # runtime env vars (PORT=1940 by default; any webhook
# API keys you add later). Sourced by the daemon wrapper.
vault.log # stdout of the running daemon (tail via `parachute-vault logs`)
vault.err # stderr of the running daemon
server-path # text file: absolute path to the repo's src/server.ts —
# how the daemon wrapper finds the source after you move it
start.sh # the wrapper launchd/systemd execs. Knows the absolute
# path to `bun` so a later PATH change doesn't break the daemon
assets/ # legacy top-level uploads dir (attachments now land per-vault)
vaults/ # one subdirectory per vault
default/
vault.db # the SQLite database — notes, tags, links, attachments,
# per-vault tokens, OAuth clients + codes, tag schemas
vault.yaml # per-vault config — description (sent as MCP session
# instruction), published_tag override, legacy api_keys
assets/ # per-vault uploaded attachments (audio, images)~/.parachute/ itself is the ecosystem root shared across sibling services — services.json and well-known/ live at the root and are managed by the top-level CLI. Everything vault owns is scoped under ~/.parachute/vault/. Pre-0.3 installs kept vault state directly at the root; any legacy paths still there are auto-migrated into vault/ on first post-upgrade run (see CHANGELOG).
config.yaml is the one file written at 0600 because it holds the bcrypt owner-password hash and the plaintext TOTP secret. .env is written with your umask default (typically 0644); if you add webhook API keys there, tighten the mode yourself. SQLite DBs follow your umask.
Registered externally
- macOS: a launchd user agent labelled
computer.parachute.vault(pluscomputer.parachute.vault.backupif you ranvault backup --schedule). - Linux + systemd: a user service named
parachute-vault.service(managed viasystemctl --user). - Neither of the above:
vault initprints a reminder to start the server yourself (bun src/server.tsor Docker). No service registration.
The daemon binds 0.0.0.0:1940 (or whatever you set in PORT) and serves REST, MCP, and OAuth routes. parachute-vault status is the fast check; parachute-vault url prints just the URL for use in scripts.
~/.claude.json
vault init adds one entry — mcpServers["parachute-vault"] — pointing at http://127.0.0.1:<port>/vault/<default-vault>/mcp with a baked-in Authorization: Bearer pvt_... header. Next Claude Code session picks it up; there's no further wiring. See Connecting a client for rotating that token or pointing it elsewhere.
Your API token
vault init asks two explicit questions: (1) install vault as an MCP server in ~/.claude.json? (2) also surface the API token so you can paste it into other MCP clients (Codex, Goose, OpenCode, Cursor, Zed, Cline), scripts, or curl? Both default yes. Pass --mcp / --no-mcp and --token / --no-token for non-interactive installs.
If you said yes to (2), the pvt_... token is printed prominently at the end — it's the same token baked into ~/.claude.json (if you also said yes to (1)). It's not stored anywhere retrievable — save it if you need it for curl, cron, or any other script. Lost it? Just mint a new one: parachute-vault tokens create. Tokens are SHA-256 hashed at rest in each vault's vault.db.
Owner password (for OAuth, coming soon)
vault init doesn't prompt for an owner password — the password is only needed for OAuth consent, which is what browser-based clients (claude.ai, ChatGPT, Claude Desktop) use, and those paths are coming in the next few weeks. When you're ready to expose the vault publicly, set one with parachute-vault set-password (and optionally parachute-vault 2fa enroll). See Connecting a client → Owner password.
Connecting a client
Two ways to authenticate — pick based on the client, not the deployment:
| Path | When to use | User action |
|---|---|---|
| OAuth 2.1 + PKCE (browser flow) | Claude Desktop, Parachute Daily, any third-party MCP client set up interactively | Click "Add integration", enter server URL, a browser opens to the vault's consent page, you enter the owner password, done — no token ever touches your clipboard |
| Bearer token | Claude Code (auto-wired by vault init), CLI scripts, cron jobs, any non-interactive caller | curl -H "Authorization: Bearer pvt_..." — the token is printed once at vault init (save it) or minted on demand with parachute-vault tokens create |
Both paths end up with the same kind of token in the vault's DB — a pvt_ string, scoped to one vault and one permission level (full or read). OAuth just moves the "how does the client get that token" step from "human copy-pastes it" to "browser-based handshake with the owner's consent."
Owner password (needed for OAuth)
vault init prompts you to set an owner password (minimum 12 characters). This is what the OAuth consent page asks for when a client requests access. If you skip the prompt, OAuth still works but the consent page falls back to asking for a vault token instead — functional but clunky. Set it later with:
parachute-vault set-password # set / change
parachute-vault set-password --clear # remove (reverts to token fallback)
parachute-vault 2fa enroll # optional: add TOTP 2FA on topPassword and 2FA secrets live in ~/.parachute/vault/config.yaml at mode 0600 (bcrypt hash + base32 TOTP secret).
Claude Code
vault init fully auto-configures ~/.claude.json — there's nothing else to do. The entry it writes uses a baked-in pvt_ token rather than OAuth:
{
"mcpServers": {
"parachute-vault": {
"type": "http",
"url": "http://127.0.0.1:1940/vault/{name}/mcp",
"headers": { "Authorization": "Bearer pvt_..." }
}
}
}Where {name} is default on a fresh install, or whatever vault you pointed vault init at. First MCP call after vault init requires no browser handoff — Claude Code uses the baked-in token and the vault's tools show up in your next session. This is intentional: for an owner connecting their own machine's vault to their own Claude Code, the token is already there and OAuth would add friction.
To re-point Claude Code at a different vault, change default_vault in ~/.parachute/vault/config.yaml and re-run parachute-vault init — which re-mints an API token and re-writes the ~/.claude.json entry end-to-end. To rotate the token only, edit ~/.claude.json and replace the Authorization header value with a fresh token from parachute-vault tokens create. (Running parachute-vault mcp-install on its own overwrites the MCP entry without an Authorization header and is intended for the rare case where you want to drop the token and connect via OAuth instead.)
Claude Desktop (OAuth)
For Claude Desktop — or any install where the server is on a different machine from the client — use the browser-based OAuth flow:
- Claude Desktop → Settings → Integrations → Add MCP server.
- Enter the URL:
https://vault.yourdomain.com/vault/{name}/mcp(replace{name}with your vault name — e.g.default). Do not paste a bearer token — leave the auth field empty. - An OAuth-capable MCP client discovers the vault's authorization server at
/vault/{name}/.well-known/oauth-authorization-server, registers itself via Dynamic Client Registration (RFC 7591), and opens your browser to the vault's consent page. - Enter your owner password (plus TOTP code / backup code if 2FA is enabled), pick a scope (
fullorread), click Authorize. - Browser redirects back. The connection is live. The client now holds a
pvt_token scoped to this vault.
If you'd rather skip OAuth — e.g. you're scripting the setup — Claude Desktop also accepts a bearer token via the integration's auth header field. Use a token from parachute-vault tokens create (or the one from vault init if you still have it). This is the "manual bearer" fallback; OAuth is the recommended path.
Parachute Daily (mobile)
Daily uses the same OAuth flow. On first launch: enter the server URL, pick the vault from the drop-down (populated from the public GET /vaults/list endpoint), tap Connect to Vault. The same consent-page handoff runs in your phone's browser, then redirects back to the app via the parachute://oauth/callback deep link. The app stores the pvt_ token in platform secure storage.
Multi-vault
One server, many vaults. Each vault is its own SQLite DB with its own MCP endpoint, its own OAuth, and its own tokens.
parachute-vault create work # new vault named "work"
parachute-vault list # show all vaults on this server
parachute-vault remove work --yesThe default vault is managed for you. vault init creates default on first install and records it as default_vault in ~/.parachute/config.yaml. vault create <name> promotes the newly-created vault to default when no default exists or when the configured default points at a missing vault. vault remove <name> promotes the sole survivor when you delete the default and one vault remains; if multiple remain after removing the default, it clears the setting and tells you to edit config.yaml yourself. There is no vault set-default subcommand — to point the server at a different existing vault, edit the default_vault: line in ~/.parachute/config.yaml and parachute-vault restart.
URL shape. Every vault-touching route lives under /vault/{name}/...: /vault/{name}/mcp, /vault/{name}/oauth/authorize, /vault/{name}/api/notes, /vault/{name}/view/{id}. There is no unscoped fallback — pick the vault in the URL even if you only have one. OAuth tokens are scoped to the vault in their issuing URL; cross-vault substitution is rejected at the OAuth layer (an auth code minted for one vault cannot be redeemed at another vault's token endpoint).
Listing vaults from a client. The authenticated GET /vaults endpoint returns full vault metadata. The public GET /vaults/list endpoint returns names only, no metadata, no auth required — this is what Parachute Daily's vault picker calls before the user authenticates. Operators who want to hide the vault list from unauthenticated callers can set discovery: disabled in ~/.parachute/vault/config.yaml to make /vaults/list return 404.
CLI
# Setup
parachute-vault init # one-command setup (idempotent — safe to re-run)
parachute-vault status # check what's running
parachute-vault doctor # diagnose install/config issues (see Troubleshooting)
parachute-vault url # print the local server URL (for scripts)
parachute --version # print the installed version (aliases: -v, version)
parachute-vault uninstall # remove daemon + MCP entry; keeps user data
parachute-vault uninstall --wipe # ...and also remove vaults, .env, config.yaml, logs
parachute-vault uninstall --yes --wipe # scripted destructive wipe (prints an audit line)
# Vaults
parachute-vault create work # create a new vault
parachute-vault list # list all vaults (alias: `ls`)
parachute-vault remove work --yes # delete a vault (alias: `rm`)
parachute-vault mcp-install # (re)write the ~/.claude.json MCP entry for the default vault
# OAuth — owner password + 2FA
parachute-vault set-password # set/change the owner password (OAuth consent page)
parachute-vault set-password --clear # remove the owner password (falls back to vault-token auth)
parachute-vault 2fa status # show 2FA state + remaining backup codes
parachute-vault 2fa enroll # enroll TOTP (shows QR + prints one-time backup codes)
parachute-vault 2fa disable # disable 2FA (requires password or TOTP/backup code)
parachute-vault 2fa backup-codes # regenerate backup codes (invalidates the old set)
# Tokens
parachute-vault tokens # list all tokens across all vaults
parachute-vault tokens create # full-access token in the default vault
parachute-vault tokens create --vault work # ...in a specific vault
parachute-vault tokens create --read # read-only token
parachute-vault tokens create --expires 30d # expiring token (N{h|d|w|m|y})
parachute-vault tokens create --label mobile # labeled token
parachute-vault tokens revoke <token-id> # revoke (default vault; add --vault to target)
# Obsidian
parachute-vault import ~/Obsidian/MyVault # import into default vault
parachute-vault import ~/Obsidian/Work --vault work # import into a specific vault
parachute-vault import <path> --dry-run # preview without importing
parachute-vault export ./output --vault work # export a specific vault
# Config
parachute-vault config # show current configuration
parachute-vault config set KEY value # set an env var (e.g. PORT=1940)
parachute-vault config unset KEY # remove an env var
parachute-vault restart # apply config changes (bounces the daemon)
# Server
parachute-vault serve # run the server in the foreground (no daemon)
parachute-vault logs # stream vault.log + vault.err (tail -f)
# Backup
parachute-vault backup # one-shot backup to configured destinations
parachute-vault backup --schedule daily # hourly | daily | weekly | manual (macOS launchd)
parachute-vault backup status # schedule, last run, destinations, next runMCP tools (9)
Notes: query-notes (universal read — single by ID/path, filter, search, graph neighborhood), create-note (single or batch), update-note (single or batch — content, tags, links, metadata), delete-note
Tags: list-tags (with optional schema detail), update-tag (upsert description + schema fields), delete-tag
Graph: find-path (BFS between two notes)
Vault: vault-info (get/update description + stats)
Vault descriptions
Each vault teaches AI agents how to use it:
# ~/.parachute/vault/vaults/default/vault.yaml
name: default
description: |
Personal knowledge vault. Tags in use: daily, voice, reader, project.
Use lowercase tags. Tag every note. Read before writing to avoid duplicates.Sent as the MCP server instruction at session start.
Features
Wikilink auto-linking
[[wikilinks]] in note content automatically create links in the graph. Supports aliases ([[Name|display]]), anchors ([[Note#Section]]), and embeds (![[Image]]). Unresolved links auto-resolve when the target note is created later.
Obsidian import/export
parachute-vault create research
parachute-vault import ~/Obsidian/Research --vault research
parachute-vault export ./output --vault researchImports: frontmatter → metadata, #tags → tags table, [[wikilinks]] → links table, file paths → note.path. Skips duplicates.
Path conventions
Note paths work like Obsidian file paths (Projects/Parachute/README). Normalized, unique, case-insensitive resolution. Renaming a note's path updates [[wikilinks]] in other notes.
Webhook triggers
Declarative config-driven webhooks that fire when a note mutation matches a predicate. Configured in ~/.parachute/vault/config.yaml:
triggers:
- name: scribe
events: [created, updated]
when:
tags: [voice]
has_content: false
missing_metadata: [transcribed_at]
action:
webhook: http://localhost:3200/v1/audio/transcriptions
send: attachment
timeout: 120000
- name: narrate
events: [created, updated]
when:
tags: [reader]
has_content: true
missing_metadata: [audio_rendered_at]
action:
webhook: http://localhost:3100/v1/audio/speech
send: contentPredicate fields: tags (all must match), has_content (true/false), missing_metadata (keys that must be absent), has_metadata (keys that must be present).
Two-phase markers: On match, the trigger sets <name>_pending_at metadata before calling the webhook, then replaces it with <name>_rendered_at on success. This prevents re-entry and concurrent runs.
Send modes
| Mode | Request format | Response format | Use case |
|---|---|---|---|
| json (default) | POST { trigger, event, note } as JSON | { content?, metadata?, attachments?, skipped_reason? } | General-purpose webhooks |
| attachment | POST first audio attachment as multipart/form-data (Whisper API format) | { text } — written to note.content | Transcription (e.g., scribe on :3200) |
| content | POST { input: note.content, model, voice } as JSON (OpenAI TTS format) | Binary audio — saved as attachment | Text-to-speech (e.g., narrate on :3100) |
Example flow — voice note transcription:
- Daily app uploads audio via
POST /api/storage/upload, creates note taggedvoicewith no content - Trigger
scribematches (tag=voice, has_content=false, missing transcribed_at) - Vault reads the audio file from its assets dir, POSTs it to scribe as multipart/form-data
- Scribe returns
{ text: "transcribed text..." } - Vault writes text to note.content and sets
scribe_rendered_atmetadata
Example flow — text-to-speech:
- A note tagged
readeris created with text content - Trigger
narratematches (tag=reader, has_content=true, missing audio_rendered_at) - Vault POSTs
{ input: note.content }to narrate - Narrate returns audio bytes
- Vault saves the audio as an attachment and sets
narrate_rendered_atmetadata
Webhook servers (scribe, narrate) are stateless — they don't need vault's API key.
On-upload transcription
The trigger system above handles the "tag a note and let a webhook fill in content later" shape. Clients that already know at upload time that an audio file should be transcribed can take a shorter path: POST /api/notes/{id}/attachments with {transcribe: true} in the body. The vault stamps the attachment with transcribe_status: "pending" and the note with transcribe_stub: true, then a background worker drains the queue FIFO.
Enable the worker by pointing the server at a Whisper-compatible endpoint:
SCRIBE_URL=http://localhost:3200
SCRIBE_TOKEN=optional-bearer-tokenThe worker POSTs audio as multipart to ${SCRIBE_URL}/v1/audio/transcriptions and expects { text: string } back. On success it replaces the _Transcript pending._ placeholder in the note body (or the whole body if the placeholder is absent), clears transcribe_stub, and records transcript + transcribe_done_at on the attachment. If the user edited the note and cleared transcribe_stub before the transcript landed, the note is left alone — but the transcript is still stored on the attachment. Failures retry with exponential backoff up to three attempts before flipping transcribe_status to "failed".
Per-vault audio_retention controls what happens to the audio file on disk once the worker reaches a terminal state. It's readable and mutable at runtime via GET / PATCH /api/vault (under config.audio_retention), or by hand-editing vault.yaml.
keep(default) — leave the file on disk indefinitely.until_transcribed— unlink on successful transcription; on failure the file is kept so you can retry or re-upload.never— unlink on any terminal state, including failure. Users who opt in accept that losing a bad transcription also loses the source audio. The file stays during mid-queue retries so in-flight attempts still have something to send.
In every mode the attachment row (and any stored transcript) is preserved; only the file on disk is affected.
Backing up your vault
Your vault is just SQLite DBs + a handful of YAML files under ~/.parachute/vault/. parachute-vault backup snapshots everything into a single timestamped tarball, for a one-shot or a scheduled run.
parachute-vault backup # one-shot — snapshot + ship to destinations
parachute-vault backup --schedule daily # register a launchd agent (macOS)
parachute-vault backup --schedule manual # stop scheduled backups
parachute-vault backup status # schedule, last run, destinations, next runConfigure destinations in ~/.parachute/vault/config.yaml:
backup:
schedule: daily # hourly | daily | weekly | manual
retention:
daily: 7 # last 7 daily snapshots
weekly: 4 # last-of-week for 4 weeks
monthly: 12 # last-of-month for 12 months
yearly: null # last-of-year, unbounded (null = keep every year forever)
destinations:
- kind: local
path: ~/Library/Mobile Documents/com~apple~CloudDocs/parachute-backupsRetention is tiered (grandfather / father / son). After each run, the pruner keeps the union of four tiers:
| Tier | What it keeps |
|---------|--------------------------------------------------------|
| daily | The N most recent snapshots. |
| weekly | The last snapshot of each of the last N ISO weeks. |
| monthly | The last snapshot of each of the last N calendar months.|
| yearly | The last snapshot of each year — null means unbounded.|
A snapshot that qualifies for multiple tiers is kept once. Set any tier to 0 to disable it; sparse data (days without a backup) just means some tiers contribute nothing that day. Bucketing uses your local timezone, so calendars line up with what you see, not UTC.
What's in a snapshot: atomic VACUUM INTO copies of every vaults/<name>/vault.db, your config.yaml, and each vault's vault.yaml, bundled as parachute-backup-<timestamp>.tar.gz. Safe under concurrent reads/writes — no need to stop the daemon.
Restore: extract the tarball into a fresh ~/.parachute/vault/ and run parachute-vault init to re-register the daemon. The DBs and configs drop in place; you don't need any special restore command (for now — a dedicated vault restore is coming soon).
Destination kinds shipping in this release: local (any filesystem path — including iCloud Drive, a mounted external disk, or an rsync/Syncthing-backed folder). s3, rsync, and cloud destinations are planned but not yet implemented.
On Linux, scheduled runs via systemd timers are a follow-up; for now parachute-vault backup works on Linux but you'll need to wire the cron yourself.
View endpoint
Serve notes as clean HTML pages at /view/:noteId:
- Without auth: only serves notes tagged
published(or withmetadata.published: true). Returns 404 for unpublished notes. - With auth: serves any note. Pass your token via
Authorization: Bearer pvt_...header or?key=pvt_...query param. - Custom tag: set
published_tagin vault.yaml to use a different tag name (default:publish).
# ~/.parachute/vault/vaults/default/vault.yaml
published_tag: publicREST API
Every vault-touching path is scoped under /vault/{name}/ — substitute your vault name (e.g. default) for {name}:
GET/POST /vault/{name}/api/notes query or create notes
GET/PATCH/DEL /vault/{name}/api/notes/:idOrPath read, update, delete a single note
GET/POST /vault/{name}/api/notes/:id/attachments list or add attachments
GET /vault/{name}/api/tags list tags (?include_schema=true for schemas)
GET/PUT/DEL /vault/{name}/api/tags/:name get, update, or delete a tag
POST /vault/{name}/api/tags/:name/rename atomic rename (409 if target_exists)
POST /vault/{name}/api/tags/merge atomic multi-source merge into a target tag
GET /vault/{name}/api/find-path?source=...&target=... shortest path between two notes
GET/PATCH /vault/{name}/api/vault vault info (get or update description)
POST /vault/{name}/api/storage/upload upload file (audio/image)
GET /vault/{name}/api/storage/:path download file
POST /vault/{name}/mcp MCP endpoint (per-vault session)
* /vault/{name}/oauth/{register,authorize,token} OAuth 2.1 + PKCE flow
* /vault/{name}/.well-known/oauth-* RFC 8414 / RFC 9728 discovery
GET /vault/{name}/view/:idOrPath render note as HTML (public or auth)
GET /vaults authenticated: full metadata
GET /vaults/list public: vault names only (unless discovery: disabled)
GET /health health checkData model
notes (id, content, path, metadata, created_at, updated_at)
tags (name)
note_tags (note_id, tag_name)
links (source_id, target_id, relationship, metadata, created_at)
attachments (id, note_id, path, mime_type, metadata, created_at)Metadata is a JSON column. Vaults start blank — no predefined tags or schema.
Auth
All API and MCP requests require a valid API key. No exceptions — localhost gets no special treatment.
For wiring up an AI client (Claude Code, Claude Desktop, Parachute Daily), see Connecting a client above. This section covers token-level details: how to pass a key, how to manage tokens, and which endpoints are public by design (/health, /vaults/list, published notes at /vault/{name}/view/:id).
Passing the key
Tokens come in two shapes. Both work interchangeably at every authenticated endpoint:
pvt_...— per-vault scoped tokens (the modern format; whatvault initmints, what OAuth issues, whatparachute-vault tokens createproduces)pvk_...— legacy global API keys fromconfig.yaml(still honored for existing deployments)
# Header (preferred)
curl -H "Authorization: Bearer pvt_..." http://localhost:1940/vault/default/api/notes
# Alternative header
curl -H "X-API-Key: pvt_..." http://localhost:1940/vault/default/api/notes
# Query param (for /view endpoint only — convenient for browsers)
curl http://localhost:1940/vault/default/view/noteId?key=pvt_...Token management
Per-vault tokens with two permission levels:
| Permission | Can do |
|---|---|
| full | Everything (CRUD + delete + token management) |
| read | Query, list, find-path, vault-info only |
parachute-vault tokens # list all tokens
parachute-vault tokens create --vault work # full-access token
parachute-vault tokens create --vault work --read # read-only
parachute-vault tokens create --vault work --expires 30d # with expiry
parachute-vault tokens create --vault work --label phone # labeled token
parachute-vault tokens revoke <token-id> --vault work # revokeTokens are shown once at creation — save them immediately. SHA-256 hashed at rest.
Legacy API keys (pvk_...) from config.yaml still work at runtime but the vault keys CLI commands have been removed. Use vault tokens for all new keys.
Public endpoints
Three endpoints work without auth:
GET /health— returns{ status: "ok" }(no sensitive data)GET /vaults/list— vault names only (setdiscovery: disabledinconfig.yamlto hide)GET /vault/{name}/view/:noteId— serves published notes only (returns 404 for unpublished)
Network security
Vault runs HTTP on localhost. This is intentional — TLS is handled by the access layer. Never expose port 1940 directly to the internet without TLS.
All API and MCP requests require an API key. There is no localhost bypass.
For remote access, always use a TLS-terminating proxy:
| Access path | API key security | Safe? | |---|---|---| | localhost (same machine) | Never leaves machine | Yes | | Tailnet (Tailscale VPN) | WireGuard encrypted | Yes | | Tailscale Funnel (public HTTPS) | HTTPS at edge, WireGuard to machine | Yes | | Cloudflare Tunnel (public HTTPS) | HTTPS at edge, local socket to machine | Yes | | Direct LAN IP (no TLS) | Plaintext on WiFi | Avoid | | Direct internet (no TLS) | Plaintext on internet | Never do this |
Troubleshooting
parachute-vault doctor is your first stop
doctor inspects the install and prints one line per check with a status (✓ pass, ! warn, ✗ fail) and, when relevant, a suggested fix. It exits 1 on any fail and 0 otherwise. Run it any time something feels off.
The checks, in the order they're emitted:
| Check | What it verifies | Typical fix when failing |
|---|---|---|
| server-path pointer | ~/.parachute/vault/server-path exists, is non-empty, and points at a src/server.ts that actually exists. This is where the stale-path failure after a repo move shows up first. | parachute-vault init from the current repo location. |
| wrapper script | ~/.parachute/vault/start.sh exists. Without it, launchd / systemd has nothing to exec. | parachute-vault init. |
| launchd agent (macOS) / systemd service (Linux) | The daemon is registered and loaded/active. On Linux without systemd, the check is silently skipped. | parachute-vault restart or re-run vault init. |
| bun on PATH | bun is resolvable via your shell's PATH. Not required once the daemon is installed (start.sh embeds an absolute bun path at init time) but missing bun is the #1 first-time-user failure. | curl -fsSL https://bun.sh/install \| bash and restart the shell. |
| MCP entry in ~/.claude.json | An entry is present. When it is, two follow-ups: the URL's port matches the running vault's port, and the MCP URL is reachable over HTTP (any response — even 401 — counts as reachable). | parachute-vault mcp-install to rewrite the entry, or parachute-vault restart if the daemon is down. |
| port 1940 availability | Probes via lsof / ss and classifies: free, held by our daemon (pass), held by a foreign process (warn), or unknown (tool unavailable → check silently omitted). | Stop the conflicting process, or set a different PORT in ~/.parachute/vault/.env and re-run vault init. |
| backup agent (macOS, only when backup.schedule != manual) | The scheduled-backup launchd agent is loaded. | parachute-vault backup --schedule <hourly\|daily\|weekly> to reinstall the agent. |
| backup destinations (only when backup.schedule != manual) | At least one destination is configured; each configured destination is writable. | Edit ~/.parachute/vault/config.yaml under backup.destinations, or fix the path's permissions. |
Common failure modes
- Daemon won't start after a port change.
~/.parachute/vault/.envhas the newPORT=...but the daemon is still trying to bind the old one, or something else already holds the new port.parachute-vault doctorsurfaces both conditions. Fix the holder (or pick a different port) andparachute-vault restart. - MCP entry is stale after moving the repo. launchd/systemd keeps pointing at the old path.
doctorflags this as a failedserver.ts at pointer targetcheck;parachute-vault initfrom the new location rewrites the pointer, wrapper, and daemon registration. - Claude Code shows no vault tools. Check in order: (1) is the daemon up (
parachute-vault status)? (2) does~/.claude.jsonhave aparachute-vaultentry with bothurland a validAuthorizationheader? (3) does the URL's vault name match an existing vault?parachute-vault doctorcatches the first two. A missing or staleAuthorizationheader after a barevault mcp-installis the usual culprit for #2 — see the Claude Code section of Connecting a client for how to rewrite it. - Claude Desktop / Daily won't connect via OAuth. If the owner-password prompt was skipped at
vault init, the consent page falls back to requiring a vault token in place of the password (functional but clunky). Set one now withparachute-vault set-password. If 2FA is enrolled, have your authenticator app ready before starting the flow; lost TOTP access recovers via the backup codes printed at enrollment. - Scheduled backups aren't running. On macOS:
doctorflagsbackup agent: not loadedwhenscheduleisn'tmanualbut the launchd agent is missing — rerunparachute-vault backup --schedule <freq>to reinstall it. On Linux: systemd-timer support for backup isn't shipped yet, so--schedule dailysilently skips the scheduler. Runparachute-vault backupfrom cron (or similar) until that lands.
Getting help
If doctor is all-green but something still isn't working, capture the output alongside parachute-vault status and open an issue at https://github.com/ParachuteComputer/parachute-vault/issues. Redact tokens from any logs before attaching.
Deployment
Remote access via Cloudflare Tunnel (free)
Your vault needs a public HTTPS URL for Claude Desktop and mobile apps. Cloudflare Tunnel provides one for free.
# Install: Mac
brew install cloudflared
# Install: Linux
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb && sudo dpkg -i cloudflared.deb
# Quick tunnel (temporary, for testing)
cloudflared tunnel --url http://localhost:1940
# Permanent tunnel (production)
cloudflared tunnel login
cloudflared tunnel create vault
cloudflared tunnel route dns vault vault.yourdomain.com
# Write config (required for background service)
sudo mkdir -p /etc/cloudflared
sudo tee /etc/cloudflared/config.yml << EOF
tunnel: $(cloudflared tunnel list -o json | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
credentials-file: $HOME/.cloudflared/$(cloudflared tunnel list -o json | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4).json
ingress:
- hostname: vault.yourdomain.com
service: http://localhost:1940
- service: http_status:404
EOF
# Run as background service
sudo cloudflared service install
sudo systemctl start cloudflaredThen point any client at https://vault.yourdomain.com/vault/{name}/mcp. See Connecting a client → Claude Desktop (OAuth) — the flow is identical to the local case once the URL is remote; the browser-based OAuth handshake makes the connection without pasting a bearer token.
Remote access via Tailscale Funnel
If you're already on Tailscale, Funnel is the shortest path to a public HTTPS URL — no custom domain, no reverse-proxy config, no cert management. Good for a single-user vault or a vault shared with a handful of people; the edge has bandwidth and connection-count caps, so not suited to heavy traffic.
Prerequisites: Tailscale v1.52 or later (earlier versions use a two-command tailscale serve + tailscale funnel on form that is now deprecated), the tailnet must have MagicDNS and HTTPS enabled in the admin console, and the funnel node attribute must be granted in your ACLs. The CLI adds the ACL entry on first use if you're the tailnet owner.
Expose the vault:
# One command — Tailscale provisions the HTTPS cert, updates the ACL if needed,
# and registers a persistent funnel that survives reboots.
tailscale funnel --bg --https=443 localhost:1940
# See what's being served and on which tailnet hostname
tailscale funnel status
# Take it down later
tailscale funnel --https=443 localhost:1940 off
# ...or nuke the whole funnel config
tailscale funnel resetThe resulting URL is https://<your-device>.<your-tailnet>.ts.net/ — tailscale funnel status prints it verbatim. You can also use ports 8443 or 10000 via --https=<port>; no other public ports are available to Funnel.
Point any MCP client at the Tailscale URL:
- Claude Desktop → Settings → Integrations → Add MCP →
https://<your-device>.<your-tailnet>.ts.net/vault/{name}/mcp(leave the Authorization field empty; the OAuth flow will handle it — see Connecting a client). - Parachute Daily → enter the base URL
https://<your-device>.<your-tailnet>.ts.net, pick the vault, tap Connect.
Cloudflare vs Tailscale, at a glance. Pick Cloudflare when you want a custom domain, bandwidth headroom for heavier traffic, or to share the vault with people who aren't on your tailnet. Pick Tailscale when you're already running it, you're fine with a *.ts.net URL, and you want the setup to fit in two commands.
Docker
cp .env.example .env # edit with your config
docker compose up -dCloud platforms
Railway ($5/mo) — Deploy from GitHub, persistent volume, public URL.
Fly.io ($3-5/mo) — fly launch --copy-config && fly volumes create vault_data --size 1 && fly deploy
Requirements
- Bun —
curl -fsSL https://bun.sh/install | bash(bun.sh). - macOS 13+ (launchd user agent) or Linux (systemd user service; other init systems work if you start the server yourself).
- No root / sudo required —
vault initwrites to your user home (~/.parachute/) and registers the daemon in your user scope only. Never touches system paths or global services. - Not supported on Windows natively. WSL2 hasn't been tested; file an issue if you try it and want it to work.
- Optional, for remote access: cloudflared (Cloudflare Tunnel) or Tailscale — see Deployment.
License
AGPL-3.0
