mountable.io

Cookbook: a Slack message when a file changes

Check a signed files.changed webhook and post the path to a Slack incoming webhook when an agent writes a file.

An agent writes a report and the process ends. The file is still there. Nobody sees it unless they go look.

mountable.io is a persistent filesystem for AI agents. When a file is created, written, renamed or deleted, mountable.io sends your URL a signed files.changed request.

This recipe is a small receiver. It checks that signature. Then it posts the path to a Slack incoming webhook you create.

mountable.io maintains the runnable example. This is not a Slack app.

Before you start

Use an existing mountable.io account and a filesystem your API key can write. Set MOUNTABLE_API_KEY in your environment. Never put the key in a command argument.

Create an incoming webhook in Slack and keep its URL private. Slack shows the steps in Sending messages using incoming webhooks. The URL looks like https://hooks.slack.com/services/....

The receiver has to be reachable at a public https:// address on port 443. Private and loopback addresses are refused. A tunnel from that address to the process on your machine is enough. The path on the public URL is /hooks/mountable.

File writes follow the usage prices. Webhook deliveries are not billed. Signature and retry rules are in Webhooks. The contract and limits page is the filesystem contract.

Run the receiver

git clone https://github.com/writeitai/mountable.git
cd mountable/examples/slack-file-webhook
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/..."
export MOUNTABLE_WEBHOOK_SECRET="$(mountable webhooks create "$FS_ID" https://example.com/hooks/mountable --path-prefix /notes)"
python3 receiver.py

The create command prints the signing secret once on stdout and the webhook id on stderr. Keep the secret in the environment. Do not commit it. Replace the example URL with your public address.

The process listens on 127.0.0.1:8080. Point your tunnel at that port.

The first delivery is a files.resync. It can arrive before the process is listening, and mountable.io retries it. The receiver answers that check and does not post it to Slack.

The receiver remembers the last cursor in this process. An older or repeated cursor does not post again. Restarting the process forgets the cursor.

Write a file

printf 'ready\n' | mountable files write "$FS_ID" notes/report.md --if-none-match

Slack gets one line:

created /notes/report.md

A later write to the same path sends updated /notes/report.md. A rename sends deleted for the old path and created for the new one. A delete sends deleted /notes/report.md.

A burst posts the first eight paths. If more remain, the next line says how many.

A 402 with filesystem_spending_cap_reached stops that filesystem's billable work. Other filesystems keep working.

Clean up

mountable webhooks delete "$FS_ID" "$WEBHOOK_ID"

mountable webhooks list "$FS_ID" shows the webhook id if you missed it on stderr. Deleting the webhook stops further deliveries. The file stays until you delete it.

Offline check

From the example directory:

python3 -m unittest discover -p 'test_*.py'
python3 receiver.py --check

These checks sign a delivery locally. They do not call mountable.io or Slack.