> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mirage.strukto.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Slack

> Mount a Slack workspace as a virtual filesystem from Node or the browser.

MIRAGE ships `SlackVFS` in **two runtimes**:

* `@struktoai/mirage-node`, talks to `https://slack.com/api/*` directly using a bot token (and optionally a user token for `search.messages`).
* `@struktoai/mirage-browser`, stays secret-free: a small proxy server on your backend holds the token and forwards `/api/slack/*` to `https://slack.com/api/*`. The browser only ever sees the proxy URL.

Both runtimes expose the same filesystem shape (`/slack/channels/`, `/slack/dms/`, `/slack/users/`) and the same shell commands, and both pair with the [slack CLI](https://docs.mirage.strukto.ai/typescript/cli/slack.md) for acting on the workspace.

## Get a bot token

1. Visit the [Slack API basics page](https://api.slack.com/authentication/basics) and create an app for your workspace.
2. Under **OAuth & Permissions**, add the bot scopes you need. For read access:
   * `channels:history`, `channels:read`
   * `groups:history`, `groups:read` (optional, private channels)
   * `im:history`, `im:read` (optional, direct messages)
   * `mpim:history`, `mpim:read` (optional, group direct messages)
   * `users:read`
3. For the CLI `send-message` command, also add `chat:write`. The mount never posts.
4. For [`search.messages`](https://api.slack.com/methods/search.messages), Slack requires a **user token** (`xoxp-…`) with `search:read`. Bot tokens (`xoxb-…`) get `not_allowed_token_type`. Provide it via the optional `searchToken` field.
5. Install the app to your workspace and copy the **Bot User OAuth Token** (`xoxb-…`).

```bash theme={null}
# .env.development
SLACK_BOT_TOKEN=xoxb-...
SLACK_USER_TOKEN=xoxp-...   # optional, only needed for slack search
```

## Node (server-side)

```bash theme={null}
pnpm add @struktoai/mirage-node
```

```ts theme={null}
import { MountMode, SlackVFS, Workspace } from '@struktoai/mirage-node'

const slack = new SlackVFS({
  token: process.env.SLACK_BOT_TOKEN!,
  searchToken: process.env.SLACK_USER_TOKEN,  // optional
})

const ws = new Workspace({ '/slack': slack }, { mode: MountMode.READ })
const res = await ws.shell('ls /slack/channels/')
console.log(res.stdoutText)
```

### Config

| Field | Description |
| - | - |
| `timeout` default `DEFAULT_TIMEOUT` | Seconds a request may wait to connect, or between two reads of its answer, before it fails; a download that keeps flowing is never cut off. |
| `token` required, secret | Slack token every API call sends: a bot token (`xoxb-`) or a user token (`xoxp-`). |
| `searchToken` secret | User token (`xoxp-`) with `search:read`, used only for Slack's `search.*` methods, which refuse a bot token. Unset searches with `token`. |
| `baseUrl` default `'https://slack.com/api'` | Slack Web API root; override to point at a fake or proxy. |
| `startTime` | Start of the mount's time scope, inclusive: an RFC 3339 timestamp with an explicit timezone and at most millisecond precision. Unset leaves the start open. |
| `endTime` | End of the mount's time scope, exclusive, in the same format as `startTime`, which it must follow. Unset leaves the end open. |

Fields marked secret are redacted in snapshots.

## Browser

```bash theme={null}
pnpm add @struktoai/mirage-browser
```

The browser package never sees the bot token. Instead, point it at a relative URL that your backend proxies to `https://slack.com/api/*`, attaching the `Authorization: Bearer …` header server-side.

### Config

| Field | Description |
| - | - |
| `timeout` default `DEFAULT_TIMEOUT` | Seconds a request may wait to connect, or between two reads of its answer, before it fails; a download that keeps flowing is never cut off. |
| `startTime` | Start of the mount's time scope, inclusive: an RFC 3339 timestamp with an explicit timezone and at most millisecond precision. Unset leaves the start open. |
| `endTime` | End of the mount's time scope, exclusive, in the same format as `startTime`, which it must follow. Unset leaves the end open. |
| `proxyUrl` required | URL of your backend proxy, which forwards to `https://slack.com/api` and attaches the token. A path resolves against the page's origin; no query string or fragment. |
| `getHeaders` secret | Function returning extra headers sent with every request to the proxy, such as your own session credential. |

Fields marked secret are redacted in snapshots.

### 1. Server: minimal proxy

```ts theme={null}
import { createServer } from 'node:http'

const TOKEN = process.env.SLACK_BOT_TOKEN!
const PREFIX = '/api/slack/'

createServer(async (req, res) => {
  const url = new URL(req.url ?? '/', 'http://localhost')
  if (!url.pathname.startsWith(PREFIX)) {
    res.statusCode = 404
    res.end()
    return
  }
  const upstream = new URL(`https://slack.com/api/${url.pathname.slice(PREFIX.length)}`)
  upstream.search = url.search

  const method = (req.method ?? 'GET').toUpperCase()
  const chunks: Buffer[] = []
  if (method !== 'GET' && method !== 'HEAD') {
    for await (const chunk of req) chunks.push(chunk as Buffer)
  }
  const body = chunks.length > 0 ? Buffer.concat(chunks).toString('utf-8') : undefined

  const headers: Record<string, string> = { Authorization: `Bearer ${TOKEN}` }
  const ct = req.headers['content-type']
  if (typeof ct === 'string') headers['content-type'] = ct

  const upstreamRes = await fetch(upstream, { method, headers, ...(body !== undefined ? { body } : {}) })
  const text = await upstreamRes.text()
  res.statusCode = upstreamRes.status
  const upstreamCt = upstreamRes.headers.get('content-type')
  if (upstreamCt !== null) res.setHeader('content-type', upstreamCt)
  res.end(text)
}).listen(8901, '127.0.0.1')
```

### 2. Browser: wire it up

```ts theme={null}
import { MountMode, SlackVFS, Workspace } from '@struktoai/mirage-browser'

const slack = new SlackVFS({
  proxyUrl: '/api/slack',
  // For multi-tenant setups, return per-request auth headers:
  // getHeaders: async () => ({ Authorization: `Bearer ${userJwt}` }),
})

const ws = new Workspace({ '/slack': slack }, { mode: MountMode.READ })
const res = await ws.shell('ls /slack/channels/')
console.log(res.stdoutText)
```

> [!WARNING]
>
> Calling `https://slack.com/api/*` directly from a browser fails CORS. The proxy is mandatory, Slack does not set permissive CORS headers.

## VFS.md

The [VFS.md](https://docs.mirage.strukto.ai/home/setup/vfs-md.md) an agent reads with Slack mounted at `/<path>`.

**Rendered**

### Virtual Filesystem

| Path | Description |
| - | - |
| `/<path>` | `slack`, read |

#### `/<path>` layout

```text theme={null}
/<path>
  channels/
    <channel-name>__<channel-id>/
      <yyyy-mm-dd>/
        chat.jsonl                # messages for that date
        files/                    # attachments shared that day (may be empty)
          <name>__<F-id>.<ext>    # cat to download bytes
  dms/
    <user-name>__<dm-id>/
      <yyyy-mm-dd>/
        chat.jsonl
        files/
          <name>__<F-id>.<ext>
  users/
    <username>__<user-id>.json    # user profile
  Naming: channel/DM/user directory names are `<display-name>__<id>`.
  The display name keeps the original spelling from Slack; only `/` is
  replaced with `∕` (U+2215) so paths don't break. Quote names
  containing spaces in shell commands. Always ls the parent dir first
  to discover exact entry names (they include IDs).
  Without time bounds, bare channel listings show the latest 90 days
  ending at the newest message. Date globs can discover earlier days.
  With start_time/end_time, listings cover that scope; messages and their
  attachments outside it are absent, including through direct paths.
  Users and channel metadata remain available as context.
  On a time-scoped mount grep and rg see only the in-scope files.
  Messages are JSONL; use jq to extract fields like .text, .user, .ts, .files.
  .user is a user ID (U…), not a username; resolve it through
  users/<username>__<user-id>.json and filter messages by the ID.
  Unless walk_limit is -1, grep -rw and rg -w read only the channel
  and DM days Slack search names; attachments under files/ are always read.
  Time scope: start_time=unbounded (inclusive), end_time=unbounded (exclusive). Explicit paths and globs cannot escape this scope.
```

#### Supported Unix Commands for VFS

Unix commands work on each path as on a local filesystem, as far as the path supports them:

| Path | Supported |
| - | - |
| `/<path>` | Commands that read files (`ls`, `cat`, `grep`, `find`, `jq` and the rest). `grep -r` and `rg` with `-w` ask `slack` which files may match. |

#### Limitations of Unix Commands for VFS

Where a path differs from a local filesystem:

| Path | Limitations |
| - | - |
| `/<path>` | Read only: no command changes files. Without `-w`, `grep -r` and `rg` read every file. `slack` answers only for `channels/*/*/chat.jsonl` and `dms/*/*/chat.jsonl`; `grep -r` and `rg` read every other file. `grep -r`, `find` and `du` stop after 1000 files; a glob matching more, `tar -c`, `cp -r` and `rm -r` are refused. |

Run `man` to list every command, and `man <cmd>` or `<cmd> --help` for one command's usage.

**Raw**

````markdown theme={null}
---
name: workspace-filesystem
description: The virtual filesystem layout of this workspace.
---

# Virtual Filesystem

| Path | Description |
| --- | --- |
| `/<path>` | `slack`, read |

## `/<path>` layout

```text
/<path>
  channels/
    <channel-name>__<channel-id>/
      <yyyy-mm-dd>/
        chat.jsonl                # messages for that date
        files/                    # attachments shared that day (may be empty)
          <name>__<F-id>.<ext>    # cat to download bytes
  dms/
    <user-name>__<dm-id>/
      <yyyy-mm-dd>/
        chat.jsonl
        files/
          <name>__<F-id>.<ext>
  users/
    <username>__<user-id>.json    # user profile
  Naming: channel/DM/user directory names are `<display-name>__<id>`.
  The display name keeps the original spelling from Slack; only `/` is
  replaced with `∕` (U+2215) so paths don't break. Quote names
  containing spaces in shell commands. Always ls the parent dir first
  to discover exact entry names (they include IDs).
  Without time bounds, bare channel listings show the latest 90 days
  ending at the newest message. Date globs can discover earlier days.
  With start_time/end_time, listings cover that scope; messages and their
  attachments outside it are absent, including through direct paths.
  Users and channel metadata remain available as context.
  On a time-scoped mount grep and rg see only the in-scope files.
  Messages are JSONL; use jq to extract fields like .text, .user, .ts, .files.
  .user is a user ID (U…), not a username; resolve it through
  users/<username>__<user-id>.json and filter messages by the ID.
  Unless walk_limit is -1, grep -rw and rg -w read only the channel
  and DM days Slack search names; attachments under files/ are always read.
  Time scope: start_time=unbounded (inclusive), end_time=unbounded (exclusive). Explicit paths and globs cannot escape this scope.
```

## Supported Unix Commands for VFS

Unix commands work on each path as on a local filesystem, as far as the path supports them:

| Path | Supported |
| --- | --- |
| `/<path>` | Commands that read files (`ls`, `cat`, `grep`, `find`, `jq` and the rest). `grep -r` and `rg` with `-w` ask `slack` which files may match. |

## Limitations of Unix Commands for VFS

Where a path differs from a local filesystem:

| Path | Limitations |
| --- | --- |
| `/<path>` | Read only: no command changes files. Without `-w`, `grep -r` and `rg` read every file. `slack` answers only for `channels/*/*/chat.jsonl` and `dms/*/*/chat.jsonl`; `grep -r` and `rg` read every other file. `grep -r`, `find` and `du` stop after 1000 files; a glob matching more, `tar -c`, `cp -r` and `rm -r` are refused. |

Run `man` to list every command, and `man <cmd>` or `<cmd> --help` for one command's usage.
````

```text theme={null}
/slack/
  channels/
    <channel-name>__<channel-id>/
      <yyyy-mm-dd>/
        chat.jsonl
        files/
          <name>__<F-id>.<ext>
  dms/
    <user-name>__<dm-id>/
      <yyyy-mm-dd>/
        chat.jsonl
        files/
          <name>__<F-id>.<ext>
  users/
    <username>__<user-id>.json
    ...
```

Each channel or DM directory contains day-partitioned directories for the last 90 days (or since channel creation). Each date directory contains `chat.jsonl` plus a `files/` directory for attachments shared that day. Each user file is the full profile JSON returned by `users.profile.get`. The Slack ID is embedded after `__` in directory and file names so you can extract it for the VFS-specific commands without an extra lookup.

## Shell commands

Every standard MIRAGE shell command works on the mounted Slack tree:

| Command | Notes |
| - | - |
| `ls` | List channels, DMs, users, dates |
| `cat` | Read `chat.jsonl`, user `.json`, or files |
| `head` / `tail` | First/last N lines |
| `grep` / `rg` | Pattern search (file or directory level) |
| `jq` | Query JSON, once per JSONL line (`-s` all) |
| `wc` | Line/word/byte counts |
| `stat` | File metadata (name, size, type) |
| `find` | Recursive search with `-name`, `-maxdepth` |
| `tree` | Directory tree view |
| `pwd` / `cd` | Navigate the virtual tree |
| `echo` (glob) | Glob expansion via the derived `glob` op |

Acting on Slack (sending, reacting, pins, member info, search) goes
through the [slack CLI](https://docs.mirage.strukto.ai/typescript/cli/slack.md) when installed; the
mounted tree stays read-oriented.

## Integration

What Slack answers through its own API, so fewer files are read. [Integration](https://docs.mirage.strukto.ai/home/integration/overview.md) explains each command.

| Command | On Slack |
| - | - |
| `grep -r`, `rg` | Slack search, for whole words, with a user token. It can miss recent messages, words only in other fields (bot names and link previews). |
| `tail -f` | Each new message as it is posted |

The [Python Slack docs](https://docs.mirage.strukto.ai/python/vfs/slack.md#integration) list when each search answers and what it can miss.

## Troubleshooting

> **`not_allowed_token_type` on `slack search`**
>
> The account CLI's `slack search` command calls `search.messages`, which requires a **user token** (`xoxp-…`) with `search:read` scope. Include `searchToken` in the config passed to `ws.registerCli`; see the [Slack CLI setup](https://docs.mirage.strukto.ai/typescript/cli/slack.md). Without it, `slack search` fails. VFS `grep` and `rg` still scan the mounted files; they simply skip the optional Slack-search optimization.

> **CORS error in browser**
>
> The browser cannot call `https://slack.com/api/*` directly, Slack does not set permissive CORS headers. You must run the proxy server shown above (or your own equivalent) and point `proxyUrl` at it.

> **`rate_limited` from Slack**
>
> `SlackVFS` uses an `IndexCacheStore` (default TTL 600s) to deduplicate channel / user / date listings, but high-volume reads of `.jsonl` files can still hit Slack's per-method rate limits. Cache hits avoid the API entirely; raise the index `ttl` (`index: { ttl }`) together with the mount's `ttl` if your workspace changes slowly.
>
> History limits depend on app distribution. Check Slack's current
> [`conversations.history` method limits](https://docs.slack.dev/reference/methods/conversations.history/)
> for your installation.

## Examples

* [`examples/typescript/vfs/slack/slack.ts`](https://github.com/strukto-ai/mirage/blob/main/examples/typescript/vfs/slack/slack.ts), shell commands against `/slack/` (`ls`, `cat`, `grep`, `jq`, `tree`, `find`, `cd`, glob).
* [`examples/typescript/vfs/slack/slack_vfs.ts`](https://github.com/strukto-ai/mirage/blob/main/examples/typescript/vfs/slack/slack_vfs.ts), `patchNodeFs(ws)` so native `node:fs` calls route through the workspace.
* [`examples/typescript/vfs/slack/slack_fuse.ts`](https://github.com/strukto-ai/mirage/blob/main/examples/typescript/vfs/slack/slack_fuse.ts), FUSE-mount the workspace so external processes can browse `/slack/` as a real filesystem.
* [`examples/browser/vfs/slack/`](https://github.com/strukto-ai/mirage/tree/main/examples/browser/vfs/slack), proxy server + `@struktoai/mirage-browser` SlackVFS demo.

See [Python Slack VFS](https://docs.mirage.strukto.ai/python/vfs/slack.md) for the equivalent Python wiring.

## Time scope

Set `start_time` and/or `end_time` in the mount's YAML `config` (or pass
`start_time`/`end_time` to Python; `startTime`/`endTime` to the TypeScript
constructor). Bounds are RFC3339 timestamps with an explicit timezone and
at most millisecond precision. Start is inclusive; end is exclusive.
An omitted or null bound is open. Start must precede end.

```yaml theme={null}
mounts:
  /history:
    vfs: slack
    mode: read
    config:
      token: "<bot-token>"
      start_time: "2026-06-01T10:00:00Z"
      end_time: "2026-06-02T00:00:00Z"
```

With a bound configured, channel listings cover the selected period rather
than the default recent window. Day directories use UTC. Messages are selected
by creation time, and attachments follow their parent messages. Direct paths,
`stat`, date globs, and `grep`/`rg` cannot escape the scope: a search only rules
out files, so scoped searches scan the filtered files. User/member
and channel/server metadata remain visible as context.

These settings apply to the VFS, not to separately installed account CLIs.
`mode: read` prevents mount writes; it does not freeze upstream edits or deletions.
Rebuild/remount the VFS to change its scope so cached listings and bytes are cleared.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.