Mountable Rails engine that records deployments and exposes a small HTTP API. Install it in the app you deploy with Kamal; the generated Kamal hooks report each deploy to it.
This is one half of Crumb — the other is the crumb-mcp server
developers run locally to query the data.
Add the gem (it lives in the engine/ subdirectory of the repo):
gem "crumb", github: "unagisoftware/crumb", glob: "engine/*.gemspec"bundle installMount the engine wherever you want the API to live:
# config/routes.rb
mount Crumb::Engine, at: "/crumb"Install the migrations and run them:
bin/rails crumb:install:migrations
bin/rails db:migrateRun the generator to scaffold the initializer and Kamal hooks:
bin/rails generate crumb:installThis creates:
config/initializers/crumb.rb— sets the ingest secret fromCRUMB_INGEST_SECRET..kamal/hooks/crumb-env— the one file you edit: the Crumb API URL per deploy destination, and how the ingest secret is resolved..kamal/hooks/crumb-pre-deploy/crumb-post-deploy— the recording logic (owned by Crumb; the generator overwrites these on re-run)..kamal/hooks/pre-deploy/post-deploy— thin wrappers that call the sub-hooks. If you already have these hooks, the generator appends the Crumb call instead of overwriting them.
It also adds CRUMB_INGEST_SECRET to the env.secret list in your config/deploy*.yml
when it finds a top-level env.secret block.
CRUMB_INGEST_SECRET is a shared secret that authenticates writes from the deploy hooks
to the API. The same value must be available in two places:
-
In the app (so it can validate incoming writes) — delivered via Kamal as an
env.secret. The initializer reads it:# config/initializers/crumb.rb Crumb.configure do |c| c.ingest_secret = ENV.fetch("CRUMB_INGEST_SECRET", nil) end
-
In the deploy hook's local environment (so it can authenticate the POST/PATCH). Export it before deploying, or fetch it from your secret manager inside
crumb-env(a Bitwarden-via-Kamal example is included in the generated file).
Generate a value with e.g. openssl rand -hex 32 and store it in your secret manager.
Edit .kamal/hooks/crumb-env and set the Crumb mount URL for each Kamal destination:
case "$DEST" in
production) CRUMB_API_URL="https://your-app.com/crumb" ;;
staging) CRUMB_API_URL="https://staging.your-app.com/crumb" ;;
*) echo "Crumb: no API URL configured for destination '$DEST' — skipping" >&2; exit 0 ;;
esacNo destination is assumed: a kamal deploy without -d falls through to the *) branch
and skips recording (non-fatal) rather than guessing.
A missing CRUMB_INGEST_SECRET aborts the deploy (exit 1) — once installed, recording is
mandatory rather than silently skipped. Other conditions stay non-fatal: if the API is
unreachable or returns an unexpected response, the hooks log a notice and exit 0, so a
transient outage never blocks a deploy. See also the integrity guards below.
crumb-pre-deploy aborts a production deploy if the working tree is dirty or has
unpushed commits, so the recorded SHA always matches something reachable on the remote.
On other destinations it records the condition in the deploy's metadata instead.
Read endpoints (used by crumb-mcp) require a per-developer access token. Tokens live in
the deployed app's database, so mint them against the target environment with Kamal:
kamal app exec -d <env> "bin/rails crumb:tokens:mint OWNER=you@example.com"The raw token is printed once — only its SHA-256 digest is stored. Hand it to the MCP
config (token_env) on the developer's machine.
Against a local/dev database, run the task directly:
bin/rails crumb:tokens:mint OWNER=you@example.comMounted under wherever you put the engine (e.g. /crumb):
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST |
/deploys |
ingest secret | Open a deploy record; returns deploy_id + previous_sha |
PATCH |
/deploys/:id |
ingest secret | Close it with status, commits, changed files |
GET |
/deploys |
read token | Recent deploys (?limit=, ?touching=<path prefix>) |
GET |
/deploys/:id |
read token | One deploy with commits and changed files |
MIT.