Skip to content
HedronitePublic

About

File custody for humans and agents (GDE1 vaults)

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

Geode product mark

Geode

Stone outside. Structure inside.
File and directory custody for a shared disk, for people and agents.

CI Geode latest release geode-grotto on crates.io geode-cli on crates.io Rust 1.89 Apache-2.0 license

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

Quick start

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.txt

Pass --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.txt stores note.txt. Pass --as api/svc-a.txt to pick the vault path; --as takes one file, not a directory.
  • A directory keeps paths relative to that directory: seal ./site stores site/index.html as index.html.
  • Sealing a path that is already in the vault replaces it, exits 0, and prints warning: replacing existing vault path PATH on stderr. With --seal-names, PATH is the stored ciphertext name.

Run geode --help, or geode <command> --help, for the full command list.

What you get

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.

Agent use: skills + CLI first, MCP still shipped

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/.

Agents

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.gtok

On 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 serve serves the MCP tools geode_list, geode_read, and geode_write, either over --stdio or on a mode-0600 Unix socket (--socket PATH). Pick one.
  • Pass the token as --token-file (mode 0600, owned by you), --token-fd, or $GEODE_TOKEN. Raw --token on argv still works, but it warns, because ps and shell history can see it.
  • A token with read reads plaintext. A token with inject and no read can use a secret without seeing it. geode_read then 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 scope checks that a path under an allow prefix is still in scope. It takes no token and does not touch the vault.
  • examples/facet-geode.yaml drives the agent tools from a Facet collection. The token and key path come from the environment, never from literals.

Using a secret without reading it

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 prod

The 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 prod

The 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 gets GEODE_TOKEN.

  • A bare --allow-cmd /absolute/canonical/path allows 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"]}

    path is an absolute canonical executable. A relative path, or a symlink that resolves to a different file, is refused. sha256 is the SHA-256 of that file and is required on this form (lowercase hex; uppercase is accepted). args_prefix is 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 /tmp is refused. A release build has no switch that moves that root or the expected owner. See Security notes.

  • --require-pin, or "require_pin": true on an --allow-cmds-file object, refuses any allow rule that has no sha256, 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 sha256 either 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 run needs 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 verify checks the chain. A started row is written before any secret reaches a child; if any row cannot be written, the run fails closed (audit_failed).

Custody daemon

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.

How it works

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 GEODE sentinel, MAC'd header.json and manifest.json, and object files under epochs/.
  • 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 PATH wraps 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 DIR starts a new epoch. To revoke someone completely, use --drop-recipient ID --reseal. A drop without --reseal still leaves them able to read the old objects.
  • --seal-names stores ciphertext filenames (HCTR2-256, length-preserving). The golden vector is vectors/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 unsafe everywhere except geode-os, the small crate that turns off core dumps and spawns geode run children.
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

Security notes

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.

Limits

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.

Status

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 the inject token operation.
  • geode sign pub and geode sign blob are still there.
  • A token with read still reads plaintext.
  • The exit codes and the MCP tools geode_list, geode_read, and geode_write behave as they did in 1.0 for any token 1.1.0 accepted. geode run --file and geode run --socket exit 1 in this release.

CI (.github/workflows/ci.yml) runs cargo test --workspace --locked and cargo clippy -D warnings on Ubuntu.

Contributing

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 warnings

Release notes live in CHANGELOG.md. Conformance vectors live in vectors/v1/.

Credits and license

Geode is built and maintained by Hedronite. Apache-2.0; see LICENSE. The Geode name belongs to Hedronite.

About

File custody for humans and agents (GDE1 vaults)

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages