Docs

almost is a small command your agent runs when something happens. One line installs it, and it works the same whether you are alone or on a team of thirty.

01Install

One command. It installs almost and wires it into every agent it can find on the machine.

Nothing else is required. Desktop notifications work immediately, with no account and no network calls.

To check it without waiting for an agent, send yourself an event by hand:

echo '{"message":"hello"}' | npx almost-sh notification
02Agents

Claude Code is configured automatically. init writes Notification and Stop hooks into ~/.claude/settings.json, backing the file up first and leaving anything already there untouched.

{
  "hooks": {
    "Stop": [
      { "matcher": "",
        "hooks": [{ "type": "command",
                    "command": "npx almost-sh stop",
                    "timeout": 10 }] }
    ]
  }
}

Any other agent works the same way, because almost is just a command. If your tool can run a shell command when a task ends, it can run this:

almost stop      # the run finished
almost notification   # the run needs a person
almost idle      # the run has gone quiet

Anything on stdin is read as JSON and used to fill in the message. If it is not JSON, it is used as the message directly, so piping plain text works too.

03Teams

Sign in and you get a team with its own ingest key. Connecting a machine to that team is one command:

npx almost-sh connect alm_your_ingest_key

From then on, every event that machine produces is recorded on your board and sent to the channels your team has configured. Desktop notifications keep working exactly as before.

Everyone on the team runs the same command with the same key.

04Channels

Channels are added on the board, not in a config file, so nobody has to redeploy to change where alerts land.

  • SlackAn incoming webhook URL.
  • DiscordA channel webhook URL.
  • TelegramA bot token from @BotFather, plus a chat id.
  • WebhookAny endpoint that accepts a JSON POST.

A generic webhook receives the readable sentence and the raw event together, so you can route on the fields:

{
  "text": "ada's claude finished \u201crefactor auth\u201d in befyr/almost.",
  "event": {
    "actor": "ada",
    "agent": "claude",
    "kind": "stop",
    "task": "refactor auth",
    "repo": "befyr/almost"
  }
}

If you would rather skip the account entirely, the CLI still honours a single webhook straight from the environment:

export ALMOST_SLACK_WEBHOOK="https://hooks.slack.com/services/..."
05Privacy

almost is a signal, not a logger. It needs to know that something happened, almost never what the something was.

Two rules hold no matter how it is configured. Your transcript is never opened: Claude Code hands hooks a transcript_path pointing at the whole conversation on disk, and almost drops that key on arrival. And nothing is sent anywhere until you connect a team or configure a webhook.

By default the agent's own words never leave the machine. A permission prompt becomes a category, not a quote:

# what the agent handed the hook
"Claude needs your permission to run: rm -rf /etc/secrets"

# what almost transmits
"message": "permission required"

The desktop banner is the exception, and only because it never crosses the network. Locally you see the full detail. Over the wire you get metadata.

You do not have to take that on faith. Pipe a real payload through preview and it prints the exact request body and sends nothing:

$ almost preview notification < payload.json

mode        metadata (default)
destination https://almost.sh/api/events

{
  "kind": "notification",
  "agent": "claude",
  "actor": "ada",
  "repo": "befyr/almost",
  "session_ref": "a447ee1578a84992",
  "task": null,
  "message": "permission required"
}

Dropped before sending: transcript_path, cwd

The session id is hashed, so runs group into one row on the board without the identifier tying back to a file on your disk. If you want richer context and accept the trade, set privacy to full to include task names and messages verbatim. The transcript path is dropped even then.

06Event API

The CLI is a convenience. The endpoint underneath is plain HTTP, so anything that can make a request can report in.

curl -X POST https://almost.sh/api/events \
  -H "Authorization: Bearer alm_your_ingest_key" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "stop",
    "agent": "claude",
    "actor": "ada",
    "task": "refactor auth middleware",
    "repo": "befyr/almost",
    "session_ref": "abc123"
  }'

kind is one of start, stop, notification, idle or done, and each one moves the card:

  • startRunning
  • notificationBlocked
  • idleBlocked
  • stopAlmost
  • doneDone

stop lands in Almost rather than Done on purpose: an agent stopping is not the same as the work passing. Send done from whatever your real gate is and the card closes without anyone touching the board:

# in CI, after the suite passes
curl -X POST https://almost.sh/api/events \
  -H "Authorization: Bearer $ALMOST_INGEST_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"kind\":\"done\",\"agent\":\"ci\",\"session_ref\":\"$RUN_ID\"}"

Everything else is optional. session_ref ties several events to one run, so the board shows a single card changing state rather than five separate lines.

The response tells you how the fan out went: { "ok": true, "sent": 2, "failed": 0 }. A failing channel never fails your request.

06Configuration
  • ALMOST_SLACK_WEBHOOKPost to one webhook with no account.
  • ALMOST_INGEST_KEYTeam key, written by connect.
  • ALMOST_API_URLPoint the CLI at your own deployment.
  • ALMOST_ACTORName shown on the board. Defaults to your system user.
  • ALMOST_SILENTSet to 1 to suppress the desktop banner.

To remove almost entirely, restore the backup it wrote and uninstall the package:

cp ~/.claude/settings.json.almost-backup ~/.claude/settings.json
npm uninstall -g almost-sh