> ## 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](/typescript/cli/slack) 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. The minimum for read access is:
   * `channels:history`, `channels:read`
   * `groups:history`, `groups:read`
   * `im:history`, `im:read`
   * `users:read`
3. For posting messages, also add `chat:write`.
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)
```

## 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.

### 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.
</Warning>

## Filesystem layout

```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](/typescript/cli/slack) when installed; the
mounted tree stays read-oriented.

## Troubleshooting

<AccordionGroup>
  <Accordion title="`not_allowed_token_type` on slack search / rg /slack/">
    `search.messages` requires a **user token** (`xoxp-…`) with `search:read` scope. Set `searchToken` on the `SlackConfig`:

    ```ts theme={null}
    new SlackVFS({
      token: process.env.SLACK_BOT_TOKEN!,
      searchToken: process.env.SLACK_USER_TOKEN,
    })
    ```

    Without it, workspace-scope `rg` and `slack search` fail; channel-scope `rg` (e.g. `rg foo /slack/channels/general__C…/`) still works since it streams the JSONL files directly.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="`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; tune `indexTtl` if your workspace changes slowly. Per [Slack docs](https://api.slack.com/docs/rate-limits), Tier 3 methods like `conversations.history` allow \~50 requests / minute / workspace.
  </Accordion>
</AccordionGroup>

## Examples

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

See [Python Slack VFS](/python/vfs/slack) 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. Scoped searches scan
the filtered files instead of using unrestricted provider search. 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.