Kelrik docs
Everything you need to get going with Kelrik Terminal and its integrations.
Getting started
- Create a Kelrik account. Your password never leaves your browser: it derives the keys that encrypt your vault. Every new account includes three months of Pro.
- Open the web app in the same tab and add your first host, or import an
~/.ssh/config. - Connect a Slack, Teams, Discord, Telegram or ntfy destination so your servers can reach you.
Use the apps with this account
The Mac, iPhone and iPad apps (in beta) sign in to any Kelrik server. To use your account from this site, open Settings → Account → Kelrik server in the app and enter:
https://kelrik.app/api/v1Then sign in with your email and password. Your hosts, keys and settings sync end to end encrypted between all of your devices.
The web app runs SSH through Kelrik's gateway, because browsers can't open SSH connections themselves. The gateway only connects to public hosts; to reach servers on a private network, use the Mac or iOS app.
Your account and password
- Your password is your encryption key. Nobody can reset it, including us. If you forget it, the copies of your vault on your devices keep working, but the account can't be recovered.
- Changing your password (on the account page or in the app) needs the current one. It re-encrypts your vault key and signs out your other devices.
- Devices: the account page lists every signed-in device and lets you sign any of them out.
- Deleting your account removes your encrypted vault, devices, sessions and integrations from the server immediately.
Integrations
Integrations send a short message to the places you already watch when something on a server needs you: a long job ended, a deploy failed, a backup didn't run, or Claude Code or Codex finished or is waiting for permission.
There are two parts. Destinations are where messages go. Notify keys are private URLs your hosts and tools send messages to. Each key can send to every destination or just some. Integrations are part of Pro, which every new account has free for three months.
Destinations
Slack
- Open api.slack.com/apps → Create New App → From scratch, and pick your workspace.
- Open Incoming Webhooks, switch it on, then Add New Webhook and choose a channel.
- Copy the URL (it starts with
https://hooks.slack.com/services/) into Kelrik.
Workflow Builder trigger URLs (/triggers/) expect their own format and aren't supported.
Microsoft Teams
- In Teams, open Workflows, from the channel's ••• menu or the Workflows app.
- Choose the template Send webhook alerts to a channel, pick the team and channel, and Save.
- Copy the webhook URL it shows into Kelrik. It's on
logic.azure.comorpowerplatform.com.
Kelrik posts an Adaptive Card. The old Office 365 connector URLs (webhook.office.com) stopped working in May 2026 when Microsoft retired them.
Discord
- Open the channel's Edit Channel → Integrations → Webhooks → New Webhook.
- Choose Copy Webhook URL and paste it into Kelrik.
Messages arrive as embeds from “Kelrik”. Mentions such as @everyone are always disabled.
Telegram
- Message @BotFather, send
/newbot, and copy the token. - Open your new bot and press Start, or add it to a group or channel.
- Paste the token into Kelrik and press Find my chats, then pick the chat.
Messages come from your own bot, so they stay between you and Telegram's servers. Kelrik stores the token encrypted.
ntfy
- Install ntfy on iPhone or Android, or open its web app.
- Subscribe to the topic Kelrik suggests (it's random, because anyone who knows a topic on ntfy.sh can read it).
- For your own ntfy server or a protected topic, enter the server address and an access token (
tk_…).
Webhooks
Kelrik POSTs JSON to your https:// endpoint and signs it so you can check it came from Kelrik. See the format and how to verify it.
Webhooks can only reach public addresses.
Notify keys
Create a key on the integrations page for each host or tool, named so you'll recognise it (“prod-api-1”, “Claude Code on my laptop”). Its URL looks like this:
https://kelrik.app/n/kn_YOUR_KEYAnyone with the URL can send you messages, so treat it like a password. If one leaks, press New URL: the old one stops working right away. The quickest test:
curl -d "Hello from $(hostname)" https://kelrik.app/n/kn_YOUR_KEYFor everyday use, add the kn helper to your shell. kn "message" sends a line; kn -- command runs a command and pings you when it ends, with its exit status and how long it took:
# ~/.bashrc or ~/.zshrc
kn() {
local url='https://kelrik.app/n/kn_YOUR_KEY'
if [ "$1" = "--" ]; then
shift
local start=$(date +%s) rc
"$@"; rc=$?
curl -fsS -m 10 -o /dev/null \
--data-urlencode "title=$([ $rc -eq 0 ] && echo Done || echo Failed): $*" \
--data-urlencode "text=exit $rc after $(( $(date +%s) - start ))s" \
--data-urlencode "level=$([ $rc -eq 0 ] && echo success || echo error)" \
--data-urlencode "host=$(hostname)" "$url"
return $rc
fi
curl -fsS -m 10 -o /dev/null --data-urlencode "text=$*" --data-urlencode "host=$(hostname)" "$url"
}Cron jobs can report only their failures: 0 3 * * * /usr/local/bin/backup.sh || curl -fsS -d "Backup failed on $(hostname)" https://kelrik.app/n/kn_YOUR_KEY
Claude Code and Codex
Coding agents running on your servers can tell you when they finish a turn or need a decision, so you can close the laptop and still know when to come back. The integrations page shows these setups with your real URL filled in.
Claude Code: add to ~/.claude/settings.json on the host (merge with any hooks you already have). Notification fires when Claude needs a permission or is waiting for you; Stop fires when it finishes a turn.
{
"hooks": {
"Notification": [
{ "hooks": [{ "type": "command", "command": "curl -fsS -m 10 -o /dev/null -H 'Content-Type: application/json' -H \"X-Host: $(hostname)\" --data-binary @- 'https://kelrik.app/n/kn_YOUR_KEY' || true" }] }
],
"Stop": [
{ "hooks": [{ "type": "command", "command": "curl -fsS -m 10 -o /dev/null -H 'Content-Type: application/json' -H \"X-Host: $(hostname)\" --data-binary @- 'https://kelrik.app/n/kn_YOUR_KEY' || true" }] }
]
}
}Codex: add this line to ~/.codex/config.toml, above any [section]. Codex runs it after each turn with a JSON summary, and Kelrik turns it into “Codex finished” with the start of its last reply.
notify = ["bash", "-c", 'curl -fsS -m 10 -o /dev/null -H "Content-Type: application/json" -H "X-Host: $(hostname)" --data-binary "$1" https://kelrik.app/n/kn_YOUR_KEY || true', "kelrik-notify"]Agent messages are shortened to 400 characters. They pass through Kelrik's server to reach your chat app, so leave these hooks off on hosts whose output must never leave them.
Notify API
Send a POST (or PUT) to your key's URL. The key can also go in a header instead of the path, which keeps it out of URLs and logs:
curl -H "Authorization: Bearer kn_YOUR_KEY" -d "Deploy finished" https://kelrik.app/nKelrik understands these bodies:
| Body | What happens |
|---|---|
| Plain text | A short single line becomes the title; anything longer becomes the message. |
| JSON | title, text (or message), level, host, url. |
| Form fields | The same names: curl --data-urlencode "title=…" --data-urlencode "text=…". |
| Claude Code hook input | Recognised automatically: “Claude Code needs you” or “Claude Code finished”. |
| Codex notify JSON | Recognised automatically: “Codex finished” with the last reply. |
Headers can fill in what the body leaves out: Title, X-Level (or ntfy-style Priority 1–5), X-Host and Click (a link). level is one of info, success, warning or error, and shows as a coloured LED.
| Answer | Meaning |
|---|---|
202 | Accepted: {"ok":true,"id":"evt_…","queued":3}. Delivery happens right after; results appear on the integrations page. |
404 | Unknown key: it was deleted or replaced with a new URL. |
402 | Integrations are paused because the account's Pro trial or plan ended. |
413 | The body is over 16 KB. |
429 | Too many messages; wait the number of seconds in Retry-After. |
Verifying webhooks
Webhook destinations receive this JSON:
{
"type": "kelrik.notification",
"id": "evt_9mQ2xY7rTn0bC1dE",
"event": "agent.needs_input",
"level": "warning",
"title": "Claude Code needs you",
"text": "Claude needs your permission to use Bash",
"host": "prod-api-1",
"key": "prod-api-1",
"dir": "api",
"url": null,
"source": "claude-code",
"at": "2026-10-08T14:02:11.000Z"
}event is message, agent.needs_input, agent.finished, agent.notification, test or paused. Each request has an X-Kelrik-Signature: t=<unix time>,v1=<hex> header: an HMAC-SHA256, keyed with your destination's signing secret, of the timestamp, a dot, and the raw body. Check it, and reject old timestamps:
// Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyKelrik(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const ts = Number(parts.t);
if (!ts || Math.abs(Date.now() / 1000 - ts) > toleranceSec) return false;
const want = createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest();
const got = Buffer.from(parts.v1 || "", "hex");
return got.length === want.length && timingSafeEqual(got, want);
}# Python
import hashlib, hmac, time
def verify_kelrik(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
ts = int(parts.get("t", "0"))
if abs(time.time() - ts) > tolerance:
return False
want = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, parts.get("v1", ""))Limits
- Each key: a burst of 10 messages, then 10 a minute. Each account: 1,000 messages a day.
- Bodies up to 16 KB. Titles are cut to 120 characters and messages to 1,500 (agent replies to 400).
- Up to 12 destinations and 25 notify keys per account.
- A destination that fails 25 times in a row is paused; press Resume once it's fixed.
- Busy or briefly unreachable services get one retry. Deliveries only go to public internet addresses.
Troubleshooting
- The test message didn't arrive
- The integrations page shows the service's answer next to the destination. “Not found” usually means the webhook was deleted on the other side; remove it in Kelrik and connect a fresh one.
- curl says
Unknown notify key - The key was deleted or given a new URL. Copy the current URL from the integrations page.
- Claude Code doesn't notify
- Check that
curlis installed on the host and that the hooks are in the settings file of the user running Claude. Run the quick test from the same host to rule out the network. - Messages stopped
- If Pro has ended, Kelrik posts one “notifications are paused” message and the API answers
402. Your setup is kept.