Stone outside. Structure inside.
File and directory custody for a shared disk, for people and agents.
Quick start · What you get · Agent use · Agents · How it works · Limits · Status · Changelog · Contributing
Built by Hedronite.
Geode seals files and directory trees so people and agents can share a disk without sharing plaintext. Ciphertext lives on the disk. The identity key stays with you. An agent gets a scoped token, never the key.
The on-disk format is GDE1, cipher suite 0x01: AEGIS-256-X2, BLAKE3, Argon2id, and HCTR2-256. You work with a vault through the geode CLI, the terminal UI, or MCP tools. Coding agents should start with the skill in .agents/skills/geode/ and the CLI. A build with the fuse feature can also mount a vault on Linux.
| Format | GDE1, suite 0x01 |
| Binary | geode (geode-cli) |
| Library | geode-grotto (Rust import geode_grotto) |
| Version | 3.0.0 · release · changelog |
| Platforms | Linux and macOS |
| Rust | 1.89 |
| License | Apache-2.0 |
You need Rust 1.89 or newer. Install the CLI from crates.io, or run it from a clone.
cargo install geode-cli
# from a clone: cargo run -p geode-cli --Generate a key, create a vault, seal one file, and read it back:
mkdir /tmp/geode-demo && cd /tmp/geode-demo
geode keygen ./demo.gkey # prompts for a passphrase
geode --key ./demo.gkey vault init ./vault.geode
printf 'stone outside\n' > note.txt
geode --key ./demo.gkey seal note.txt ./vault.geode
geode --key ./demo.gkey list ./vault.geode
geode --key ./demo.gkey cat ./vault.geode note.txtPass --key or set GEODE_KEY_FILE, unless a keyring default or ~/.config/hedronite/geode/default.gkey exists ($XDG_CONFIG_HOME is honored). geode keygen with no path writes ./secret.gkey and warns you to move it. Key files are mode 0600. Never commit one; *.gkey belongs in .gitignore.
geode keygen protects the key by default: it prompts for a passphrase, or with --keychain keeps one in the OS keychain so later commands do not prompt. Upgrading from 1.2.x: scripts and CI that run geode keygen without a terminal now fail with exit 1 and write nothing. Add --keychain, or --insecure-plaintext for a throwaway test key. --password is now the default and can be dropped. Existing raw keys keep working.
Where a seal lands in the vault:
- A file is stored under its basename, so
seal ./docs/note.txtstoresnote.txt. Pass--as api/svc-a.txtto pick the vault path;--astakes one file, not a directory. - A directory keeps paths relative to that directory:
seal ./sitestoressite/index.htmlasindex.html. - Sealing a path that is already in the vault replaces it, exits 0, and prints
warning: replacing existing vault path PATHon stderr. With--seal-names,PATHis the stored ciphertext name.
Run geode --help, or geode <command> --help, for the full command list.
| Plain shared disk | Geode | |
|---|---|---|
| Bytes on disk | Plaintext files | GDE1 objects: chunked AEAD under a merklized content root |
| Filenames | Visible | Plaintext by default; seal --seal-names stores ciphertext names |
| People | Any editor | The geode CLI and the terminal UI (geode tui) |
| Agents | The same key, or raw files | Metadata skill plus the CLI, scoped GTOK tokens, and MCP tools; see Agent use |
| Integrity | Hope | geode verify: full, --cheap, or --sample P. Exit 2 means an authentication or integrity failure |
| History | Copies | Named snapshots (geode snapshot create / ls) and geode gc |
| Identity | Ad hoc | geode keygen and geode keyring: OS keyring with a 0600 file fallback |
| Git | Plaintext in history | geode git keeps sealed copies in a sidecar vault and gitignores the plaintext |
Commands at a glance:
| Area | Commands |
|---|---|
| Keys | keygen (passphrase by default, --keychain, or --insecure-plaintext), keyring list / add, sign pub, sign blob PATH |
| Vault | vault init, vault recipients, vault add-recipient, vault rotate, policy show / set / check |
| Data | seal, open, list, cat, verify, snapshot, gc |
| Mount | mount VAULT MOUNTPOINT, unmount |
| Agents | agent token issue / inspect, agent list / read / write, agent scope, agent serve |
| Secrets | run, custody unlock / lock / status, audit verify |
| Scanning | scan PATH... (--hook for git hooks and CI) |
| UI | tui [VAULT] |
geode sign pub prints the vault identity's Ed25519 public key as SPKI PEM. geode sign blob PATH signs a file's raw bytes. The signing key never leaves Geode, and cosign verify-blob can check the signature.
The default way for an agent to use Geode is the bundled skill in
.agents/skills/geode/: a short SKILL.md the harness reads to decide when to use
it, plus a Nushell body that calls the CLI with JSON output. The MCP server
(geode agent serve) is still built and shipped, and works the same as before.
The skill is metadata only. It can list entry names, check integrity, show recipients, list snapshots, show or dry-run policy, verify the audit chain, and report custody or keyring status. It does not read secret values or change custody. Entry names can themselves be sensitive.
Why the skill is the default:
- Context. An MCP server adds every tool's name, description and input schema to the model's context, often on every request, whether or not the tools are used. Many harnesses can only turn a whole server on or off. A skill costs a few dozen tokens of description until it is used.
- Determinism. The skill body is fixed code over a versioned JSON contract, so results are the same each time.
- Small models. Local models with short context windows feel the schema cost most.
Use the MCP server when your harness defers or filters MCP tool schemas (loading them only on demand, or letting you enable single tools), or when the client has no shell.
.agents/skills/geode/ is the canonical copy. The same files are checked in
under .claude/skills/geode/, .grok/skills/geode/, and .cursor/skills/geode/
as regular files, so each harness finds them. scripts/sync-skills rewrites
those copies from .agents/skills/. Codex reads .agents/skills/. Do not
replace the copies with symlinks, and do not add .omp/skills/ (omp would
shadow the other copies) or .codex/skills/.
You issue each agent a token. A token can only narrow what the key can do. Coding agents should start with the skill above. The token and MCP surface on this page is unchanged.
geode --key ./demo.gkey agent token issue \
--vault ./vault.geode --principal agent:helper \
--ops list,read --allow-prefix scratch/ --ttl 15m > helper.gtok
chmod 600 helper.gtok
geode --key ./demo.gkey agent list ./vault.geode --token-file helper.gtokOn one machine, the agent commands and geode agent serve still open the vault with the key file, so the token limits what is read or written but the key file must be on hand. To keep the key away from the agent entirely, run the custody daemon described below.
- A token carries a principal, an operation set (
list,read,write,verify,inject), path prefixes, a byte cap, and a lifetime of at most 12 hours. It never carries key material. Treat the token itself as a secret. geode agent serveserves the MCP toolsgeode_list,geode_read, andgeode_write, either over--stdioor on a mode-0600Unix socket (--socket PATH). Pick one.- Pass the token as
--token-file(mode0600, owned by you),--token-fd, or$GEODE_TOKEN. Raw--tokenon argv still works, but it warns, because ps and shell history can see it. - A token with
readreads plaintext. A token withinjectand noreadcan use a secret without seeing it.geode_readthen returns a hash, and a request for text is refused with no body.
MCP geode_read with no mode returns a hash for every token, including a token that holds read. The value is the sealed content_root, not a digest of the plaintext. Clients that omitted mode must pass "mode": "text" (or "hex"), and the token must hold read. CLI geode agent read still prints the body when the token has read. A run-only token (geode agent token issue --run-only) is the harness default: it can list, inject, and hash-read. Run-only tokens cannot read secret plaintext through Geode's read paths (reads return hashes), but any command they are allowed to run receives the secret in its environment, so only allow pinned, specific commands and never a shell.
geode agent scopechecks that a path under an allow prefix is still in scope. It takes no token and does not touch the vault.examples/facet-geode.yamldrives the agent tools from a Facet collection. The token and key path come from the environment, never from literals.
geode run starts a command with vault objects bound into its environment:
geode --key ./demo.gkey run --vault ./vault.geode \
--secret API_KEY=keys/api --allow-cmd /usr/local/bin/deploy \
-- deploy --env prodThe same command, pinned:
geode --key ./demo.gkey run --vault ./vault.geode \
--secret API_KEY=keys/api \
--allow-cmd '{"path":"/usr/local/bin/deploy","sha256":"<64 hex>","args_prefix":["--env","prod"]}' \
-- deploy --env prodThe program reads API_KEY from its own environment. Do not write $API_KEY on the command line: your shell expands it before Geode runs, and Geode refuses any argument that contains a bound value.
-
The command must be listed with
--allow-cmd. With none listed, nothing runs. The child gets a fresh environment:PATH,HOME,LANG,TERM, and the bindings. It never getsGEODE_TOKEN. -
A bare
--allow-cmd /absolute/canonical/pathallows that program with no digest check. To pin the bytes that run, pass one JSON object to--allow-cmd, or a JSON list to--allow-cmds-file:{"path":"/usr/local/bin/deploy","sha256":"<64 hex>","args_prefix":["--env","prod"]}pathis an absolute canonical executable. A relative path, or a symlink that resolves to a different file, is refused.sha256is the SHA-256 of that file and is required on this form (lowercase hex; uppercase is accepted).args_prefixis the required leading arguments after the program. It may be empty or omitted, and then any arguments are allowed. The first matching pin is the one that runs, and an empty prefix matches every argument list. -
A pin runs only when the binary and every directory from its canonical path up to
/is owned by root, is not writable by group or other, and is not a symlink. The refusal names the first component that fails, starting at/. The check runs on Linux and macOS. On macOS it is the main defense, because the kernel cannot exec the hashed file descriptor. The sticky bit is not an exception, so a pin under/tmpis refused. A release build has no switch that moves that root or the expected owner. See Security notes. -
--require-pin, or"require_pin": trueon an--allow-cmds-fileobject, refuses any allow rule that has nosha256, including a bare--allow-cmd PATH. The check is at load time, before a child starts. The default is off.{"require_pin": true, "cmds": [{"path": "/usr/bin/deploy", "sha256": "<64 hex>"}]}A JSON list is unchanged and does not turn require-pin on. Rules in the file still need
sha256either way. -
Geode opens the pinned file once, hashes that file descriptor, and execs the same descriptor. A file swapped in after the check is not what runs. On macOS there is no
fexecve; the child re-checks the path against the hashed file immediately before exec. A root-owned, non-writable path is what keeps a non-root writer from winning that window. -
A pinned run is refused when the resolved program is not pinned, the opened file's digest does not match, the arguments do not start with
args_prefix, or the path is relative or resolves through a symlink to a different file. The error names the path. It does not print a secret, the digest of a secret, or the argument list. -
Stdout carries only the child's redacted output. Stderr names which bindings were injected and never prints their values. A copy of a bound value in the output becomes
[REDACTED:NAME]. -
Redaction catches the raw value and its base64, hex, percent-encoded, and JSON-string forms. It does not catch shell quoting or any other transform. Redaction stops accidents. It does not stop a hostile command.
-
Local
geode runneeds your key and refuses a token. -
Every run and every refusal is written to an append-only, hash-chained audit log. Rows hold no secret value and no raw command line.
geode audit verifychecks the chain. Astartedrow is written before any secret reaches a child; if any row cannot be written, the run fails closed (audit_failed).
geode agent serve --custody --vault DIR runs a daemon under its own system user (geode on Linux, _geode on macOS). The daemon holds the key; agents never see the key file. You unlock it once from a terminal with geode custody unlock and check it with geode custody status. Agents in the geode-agents group connect over a fixed socket (/run/geode/custody.sock on Linux, /var/run/geode/custody.sock on macOS) with a token bound to their uid. The MCP tool geode_run appears only for a token that holds inject, and returns the command's redacted output in one result.
One rule: ciphertext on the shared disk, the identity key with the operator, a scoped token for each agent.
- A vault is a directory with a
GEODEsentinel, MAC'dheader.jsonandmanifest.json, and object files underepochs/. - Objects are sealed under an epoch key: AEGIS-256-X2 chunks, a BLAKE3 content root, a canonical-JSON manifest MAC, and a wrap of the epoch key for each recipient.
geode vault add-recipient DIR --gpub PATHwraps the current epoch key for another person's X25519 key. A recipient can read without Geode's policy, so add only people you trust with the plaintext. A token cannot add or drop recipients.geode vault rotate DIRstarts a new epoch. To revoke someone completely, use--drop-recipient ID --reseal. A drop without--resealstill leaves them able to read the old objects.--seal-namesstores ciphertext filenames (HCTR2-256, length-preserving). The golden vector isvectors/v1/name.json.- Exit codes are stable: 0 success, 1 usage or I/O, 2 authentication or integrity failure, 3 policy deny, 4 token expired or invalid, 5 locked or busy. Scripts should treat 2 differently from 1.
- The workspace forbids
unsafeeverywhere exceptgeode-os, the small crate that turns off core dumps and spawnsgeode runchildren.
Repository map
crates/geode-core/ # geode-grotto: format, crypto, vault, policy, keyring, agent ops
crates/geode-cli/ # the geode binary
crates/geode-tui/ # terminal UI (default feature tui)
crates/geode-redact/ # redaction of secret values and known key prefixes
crates/geode-os/ # core-dump refusal and the geode run child spawn
.agents/skills/geode/ # canonical agent skill: SKILL.md and a metadata-only Nushell body
vectors/v1/ # golden vectors: kdf, chunk, wrap, name
cargo install geode-cli installs the geode binary only. A pin is trusted only when the binary and every ancestor up to / is owned by root.
The Cargo feature test-trusted-root is test-only and insecure. It compiles a hidden pair of flags, --test-trusted-root and --test-trusted-uid, that move the directory and the uid a pin trusts, so tests can stage a fixture. It is off by default. A release build refuses to compile it, including cargo install geode-cli --features test-trusted-root. A debug build can still enable it. Do not ship or run a binary built with that feature. The fixture binary geode-pin-helper is built only when the feature is on, and a normal cargo install does not install it.
| Today | |
|---|---|
| Mount on Linux | Needs a build with the fuse feature (cargo install geode-cli --features fuse). Foreground by default; --daemon writes a pid file; geode unmount calls fusermount3. Read-write unless you pass --read-only. A mount lets every process of your UID bypass policy, so unmount before you leave programs unsupervised. A token cannot mount. |
| Mount on macOS | Not working yet. geode mount exits 1 until FUSE-T or macFUSE is loaded in a build that has the fuse feature. |
| Windows | Not offered. |
| Cipher suites | Suite 0x01 is the only suite. There is no post-quantum build. |
| Other formats | Geode reads GDE1 only. It does not import or decrypt another custody tool's format. |
| Redaction | Catches known encodings of a bound value. It does not stop a command that transforms or sends the value. |
| Secret scan | geode scan matches known token shapes and high-entropy strings. It misses low-entropy secrets such as Wi-Fi passphrases and short passwords, so it is a backstop: decide what to capture with an allowlist first. It also flags public keys, hashes, and other random-looking strings. Findings show a keyed fingerprint, never the value, but paths, line numbers and fingerprints in CI logs are kept by the CI provider. An allowlist works only under the key that made it. |
| Terminal UI | A human surface. It unlocks with your key, never with a token. |
| Without default features | cargo install geode-cli --no-default-features has no terminal UI; geode tui exits 1. |
| Pin trust root | Stops at / and expects root. Feature test-trusted-root can move it, is test-only and insecure, and is refused in release builds. See Security notes. |
Version 3.0.0, git tag v3.0.0. The crates geode-cli, geode-grotto, geode-tui, geode-redact, and geode-os are published at the same version.
- The format is GDE1, suite
0x01. Objects written by 1.0.0 are still readable. - 1.2 added keyring custody and secret injection:
geode run, the custody daemon,geode custody,geode audit verify, and theinjecttoken operation. geode sign pubandgeode sign blobare still there.- A token with
readstill reads plaintext. - The exit codes and the MCP tools
geode_list,geode_read, andgeode_writebehave as they did in 1.0 for any token 1.1.0 accepted.geode run --fileandgeode run --socketexit 1 in this release.
CI (.github/workflows/ci.yml) runs cargo test --workspace --locked and cargo clippy -D warnings on Ubuntu.
Issues and pull requests are welcome. There is no CONTRIBUTING.md yet. Match CI before you open a pull request:
cargo test --workspace --locked
cargo clippy --workspace --all-targets -- -D warningsRelease notes live in CHANGELOG.md. Conformance vectors live in vectors/v1/.
Geode is built and maintained by Hedronite. Apache-2.0; see LICENSE. The Geode name belongs to Hedronite.