Configuration
September 15, 2026 ยท View on GitHub
slacrawl is configured with TOML at ~/.slacrawl/config.toml by default.
Starting with Slacrawl v0.9.0, config-saving commands use owner-only permissions
(0600) on POSIX systems, including when saving existing configs. Saving removes
group/other read access; shared-account configurations must plan for owner access.
Config path resolution, runtime directories, status payloads, and token
diagnostic formatting are normalized through crawlkit. Slack token scopes,
workspace selection, API/Desktop source behavior, and schema compatibility stay
in slacrawl.
The config is designed to work with safe defaults:
- SQLite lives under
~/.slacrawl/ - Slack Desktop is enabled by default
- the desktop path is auto-detected when left blank
- Slack tokens are resolved from environment variables
- external providers receive only explicitly allowlisted environment additions
Example
version = 1
workspace_id = ""
db_path = "~/.slacrawl/slacrawl.db"
cache_dir = "~/.slacrawl/cache"
log_dir = "~/.slacrawl/logs"
[[workspaces]]
id = "T01234567"
default = true
# uses:
# SLACK_T01234567_BOT_TOKEN
# SLACK_T01234567_APP_TOKEN
# SLACK_T01234567_USER_TOKEN
[slack.bot]
enabled = true
token_env = "SLACK_BOT_TOKEN"
[slack.app]
enabled = true
token_env = "SLACK_APP_TOKEN"
[slack.user]
enabled = true
token_env = "SLACK_USER_TOKEN"
[slack.desktop]
enabled = true
path = ""
[slack.mcp]
enabled = true
base_url = "https://chatgpt.com/backend-api/wham/apps"
auth_path = "~/.codex/auth.json"
token_env = "CODEX_APPS_ACCESS_TOKEN"
account_id_env = "CODEX_APPS_ACCOUNT_ID"
protocol_version = "2025-03-26"
connector_id = ""
channel_types = "public_channel,private_channel"
page_size = 100
search_limit = 20
max_pages = 250
[sync]
concurrency = 4
repair_every = "30m"
desktop_refresh_every = "5m"
full_history = true
[search]
default_mode = "auto"
[share]
remote = ""
repo_path = "~/.slacrawl/share"
branch = "main"
auto_update = true
stale_after = "15m"
Archive Schema Upgrades
Starting with Slacrawl v0.9.0, the first writable open of a pre-v8 database upgrades
its SQLite schema version to 8 in a transaction. This is separate from the TOML
configuration version.
Read-only inspection with automatic share updates disabled can inspect a valid
v7 archive without migrating it; init only writes configuration. A command such
as sync opens the archive writable and can migrate it even if it subsequently
fails for missing credentials.
The one-time upgrade removes history_coverage_v1 checkpoints for the exact
api-bot and api-user sources. Pre-v8 checkpoints cannot reliably distinguish
locally completed scans from checkpoints imported from another archive. Messages,
retention floors and seeds, provider/MCP state, and other sync state are retained.
The migration performs no network requests or backfill and does not mark any
history complete. Later writable opens preserve newly earned checkpoints.
The next ordinary API sync treats that coverage as unknown and may repeat history
requests from the existing retention floor. Without a floor, it can scan all
accessible history, with corresponding API cost and rate-limit delays. A newer
saved message does not prove that older history was read. Successful scans earn
local completion checkpoints; interrupted scans retain their pending interval.
--latest-only selects previously observed channels; it does not bound the
history interval or request count.
API history acquires its current lower bound, upper request horizon and a fresh
attempt generation in one archive transaction. The exact primary source,
workspace, channel and normalized --since identify the scope. A newer attempt
supersedes the older one: stale history/replies responses, thread work, skip/join
records and completion cannot overwrite its state. Earlier committed pages stay
available, and the superseded caller reports failure. An already dispatched
external join cannot be undone.
A delayed clock cannot shrink the upper horizon inherited from completed or active work. Completion records the horizon actually requested, including an empty scan. Ordinary retries apply the current retention floor and never inherit Full's restore permission; explicit Since still wins over Full, and Full wins over LatestOnly. Valid old v8 checkpoints acquire generation and upper fields on their next scan; the SQLite version and share format do not change.
History acquisition validates only its selected canonical checkpoint and owned channel. Coverage decisions separately scan retained records for the exact API sources and history type. Every key must be canonical; workspace-only decisions ignore a foreign value only after validating its key. Sync and CLI Doctor use an archive-wide view, while repair and Full skip cleanup use the selected workspace. Malformed relevant records fail without resetting or deleting them. Pending work, including an empty pending bound, or a record without completed history keeps coverage partial. Missing records do not invent completion.
These checks do not fence older unconditional writers or skip diagnostics shared
by different history scopes. Status reads counts, freshness and the coverage
marker in one read-only snapshot. A historical full is displayed as partial
while retained API thread work or incomplete API history remains. Reads preserve
the raw marker and timestamp; clearing work reveals historical full only when
that marker already existed. Genuine partial, absent and other marker values are
not promoted. Malformed retained history rejects a full projection; other marker
values stay lazily checked unless Doctor needs explicit retained facts.
Sync now checks those archive-wide facts and publishes coverage in one writer transaction, together with any eligible workspace-only skip cleanup. If retained work blocks a locally full-eligible run, the previous marker and timestamp stay unchanged. Genuine partial results write partial. Validation or write failure rolls cleanup and publication back together. Workspace completion is still separate, and repair remains workspace-scoped and partial-only. These are snapshot guarantees, not newest-invocation ordering or continuously current raw markers; missing local progress does not prove completed Slack history.
Git-share snapshots retain manifest version 1 and their existing table format, but these API coverage checkpoints are local-only and are not exported. Imports consume and validate older snapshots containing them without applying them: merge preserves the destination's own checkpoints, while explicit restore clears coverage and leaves it unknown. Data-only snapshots do not back up API completion state.
Before the first writable open, stop every old process accessing the database. Complete a consistent pre-upgrade backup using SQLite's Online Backup API and verify that the backup opens independently with the expected schema and data. Retain the old binary and configuration alongside the database backup. Copying only a live database's main file can omit WAL data. Old binaries reject schema v8 on a new open, but that check does not stop already-open clients. Do not mix old and new running processes.
Recovery with an older binary requires restoring the consistent pre-upgrade
database backup and configuration after stopping the upgraded processes. Account
for writes since that backup: restoring it discards those writes unless a
separately reviewed recovery preserves them. Do not lower user_version or
attempt an in-place downgrade. A data-only snapshot readable by an older binary
does not make old-version syncing or opening an upgraded database safe.
Workspace Selection
workspace_id remains the default CLI workspace.
Use [[workspaces]] when you want separate bot/app/user tokens per Slack workspace, especially for multi-workspace API sync and live tailing:
[[workspaces]]
id = "T01234567"
default = true
[[workspaces]]
id = "T08976543"
bot_token_env = "SLACK_CLIENT_BOT_TOKEN"
app_token_env = "SLACK_CLIENT_APP_TOKEN"
user_token_env = "SLACK_CLIENT_USER_TOKEN"
Behavior:
- each workspace automatically tries
SLACK_<WORKSPACE_ID>_BOT_TOKEN,SLACK_<WORKSPACE_ID>_APP_TOKEN, andSLACK_<WORKSPACE_ID>_USER_TOKEN - top-level
enabledflags are inherited, so you do not need to repeatenabled = truefor every workspace bot_token_env,app_token_env, anduser_token_envare optional overrides when you do not want the default env naming conventionsync --source botwithout--workspaceruns against every configured[[workspaces]]entrytailwithout--workspacestarts one live tail per configured[[workspaces]]entrysearch,messages,mentions,users, andchannelsaccept--workspaceto filter the shared SQLite databaseusersandchannelsreturn 100 rows by default and accept a positive--limitoverride- if
[[workspaces]]is empty, the legacy top-level[slack.*]token config is used
Visibility Boundaries
One config/database should represent one Slack visibility boundary: the messages visible to one bot/account/profile. Use ingestion sources to decide how that archive is populated:
sync --source botis an alias forsync --source apiand uses Slack bot/user tokenssync --source mcpfetches from a Slack connector exposed by the configured HTTP JSON-RPC MCP gatewaysync --source provider:<name>imports a configured external archive through a trusted local subprocesssync --source wiretapis an alias forsync --source desktopand reads the local Slack Desktop cachesync --source allruns token-backed sync first, then desktop enrichment; external providers remain explicit[share]backs up the current DB and safely merges snapshots by default; it is not a second Slack data source- exact latest or historical replacement requires
update --restore
Keep company and personal Slack archives in separate configs, DBs, and git remotes:
# ~/.slacrawl/company.toml
db_path = "~/.slacrawl/company.db"
[share]
remote = "git@github.com:your-org/company-slacrawl-archive.git"
repo_path = "~/.slacrawl/company-share"
# ~/.slacrawl/personal.toml
db_path = "~/.slacrawl/personal.db"
[share]
remote = "git@github.com:your-user/personal-slacrawl-archive.git"
repo_path = "~/.slacrawl/personal-share"
MCP Connector Source
MCP sync is an additional ingestion source; all reads still use the local slacrawl database. It discovers Slack tools with tools/list, calls the connector for users, channels, channel history, and threads, then normalizes the results into the same archive schema used by API and desktop sync.
slacrawl sync --source mcp --workspace T01234567
slacrawl sync --source mcp --workspace T01234567 --channels C01234567 --since 1772574099.659199
The workspace ID is required because connector responses do not reliably carry archive ownership. connector_id is optional; when empty, tool discovery matches Slack tools by their names and metadata. Set it when multiple Slack connectors are exposed by one gateway.
The default transport = "http" uses Codex's connector gateway:
[slack.mcp]
enabled = true
transport = "http"
base_url = "https://chatgpt.com/backend-api/wham/apps"
auth_path = "~/.codex/auth.json"
HTTP authentication order:
- The configured
token_envand optionalaccount_id_env. CODEX_APPS_ACCESS_TOKENorCODEX_CONNECTORS_TOKEN.- The configured
auth_path, using Codex'stokens.access_tokenand optionaltokens.account_idfields.
Automatic Codex credentials are restricted to the HTTPS chatgpt.com origin,
including redirects. A custom HTTP MCP origin requires a dedicated token_env
other than the Codex fallback variables. It never falls back to Codex env/file
credentials when that dedicated variable is missing. The default Codex account
ID is not sent to custom origins; configure a dedicated account_id_env when
the custom server needs one. Explicit custom-server tokens and stdio remain
supported.
max_pages bounds the text connector's users, channels, channel-history, and thread pagination loops and the native reference adapter's users/channels loops; hitting the bound returns an error instead of silently accepting an incomplete page set. Native history/replies tools do not accept pagination arguments. The Codex HTTP connector accepts at most 20 channel or user search results per request. With include_dms omitted/true, explicit channel IDs avoid global channel and user enumeration. Normal MCP sync retries the pending checkpoint interval or overlaps the completed history-response watermark by one hour, applying current retention; stored message maxima do not establish coverage. It also rechecks persisted thread roots because Slack does not move an old root into channel history when it receives a new reply. --full removes incremental bounds, while --latest-only uses retained non-draft history or retention-seed eligibility. MCP is an explicit source and is not included in --source all.
The text connector's channel search response may omit privacy metadata. With include_dms omitted/true, those channels remain locally searchable with kind mcp_channel, recording unknown classification. Legacy publish still includes these archive rows; that kind is not an export privacy filter.
For the archived MCP reference Slack server, use stdio and export the server's required SLACK_BOT_TOKEN and SLACK_TEAM_ID variables before running slacrawl:
[slack.mcp]
enabled = true
transport = "stdio"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-slack"]
env_allowlist = ["SLACK_BOT_TOKEN"]
page_size = 100
search_limit = 100
max_pages = 250
The subprocess receives a minimal environment plus known Slack/Codex token variables and any names listed in env_allowlist; secrets are not passed through TOML fields. The reference server exposes public or explicitly configured channels and does not paginate channel history. Therefore each sync imports only the latest page_size messages returned by slack_get_channel_history; --since filters that fetched window locally and --full cannot extend the server's history window. Channel and user listing still paginate normally. The npm package is deprecated and its upstream repository is archived, but this adapter supports its published tool contract.
Tool discovery selects either the Codex Slack connector contract or the reference slack_list_channels, slack_get_channel_history, slack_get_thread_replies, and slack_get_users contract.
Native response coverage
Under every DM policy, native history and replies require ok=true before
processing that response. A missing or false value stops the sync; earlier
committed batches remain. The text connector's response contract is unchanged.
Native channel catalogs require a channels array, user catalogs a members
array, and history/replies a messages array after the existing decode, error
and success checks. Missing or null collections leave that page uncertified;
they cannot complete empty history or retire a thread job. Explicit [] remains
valid, including empty replies. This archive-certification requirement does not
claim that Slack defines every omitted/null collection as invalid. The existing
strict-catalog success check and default-catalog/users OK policies stay unchanged.
Rejected pages preserve earlier commits and retry work; corrected responses can
complete a later retry.
Native has_more=true or a nonblank response_metadata.next_cursor reports
additional pages. The reference tools cannot request those pages. Slacrawl
processes valid fetched messages and later selected conversations, then returns
an incomplete-history error instead of printing completion or advancing the
successful MCP workspace sync record. Use --source api for paginated backfill.
The flag survives local timestamp filtering and empty thread results; a later
successful response cannot clear it. Concrete request, identity, or storage
errors take precedence.
is_limited=true reports a separate Slack history/message limit. Slack documents
this flag for earlier messages beyond a free workspace's message limit; it does
not detect every access or retention restriction. Slacrawl preserves the same
successful-sync record and asks the operator to review workspace history
availability. API pagination cannot restore messages Slack does not expose.
These checks preserve valid writes, not whole-sync atomicity. Channel metadata and valid messages may change even when the old successful workspace sync record remains. The history checkpoint stays pending when history is incomplete; a completed history scan remains complete if only replies fail. No opaque server cursor is stored or printed, and these checks do not establish complete archive coverage or extend the native server's bounded history window. See Slack's history and replies contracts.
MCP history checkpoints
MCP incremental bounds come from completed channel-history scans, not the newest
stored message. A newer reply or API/Desktop row cannot move the MCP history
cutoff. Each checkpoint belongs to a workspace, channel, discovered adapter and
normalized --since scope. History timestamps must be finite numbers, and the
latest raw history timestamp is recorded even if local filtering or a richer
stored row prevents that message from being written.
The first selected scan after upgrade, snapshot import into a fresh archive, or
whole-snapshot restore has no local MCP history checkpoint. It starts without an
incremental bound, subject to the current purge floor. This may read more history
than earlier versions. --latest-only still selects channels with a non-draft
stored message or a retention seed; a checkpoint alone does not select a channel.
If this first text-connector scan needs more than slack.mcp.max_pages, repeating
the same command with the same limit reads the same prefix and fails again.
Slacrawl keeps the checkpoint pending and does not write that channel's buffered
history. It does not save an opaque cursor between invocations. Bootstrap one
channel with a temporary, larger positive page budget:
- Record the current
max_pagesin the config used by this sync. Increase it under the existing[slack.mcp]section, for example from250to500. The larger value must cover the selected history interval;500is an example, not a guaranteed channel size. It also raises the other MCP page limits and can increase memory use, requests and run time. - Run
slacrawl sync --source mcp --workspace T01234567 --channels C01234567with that same config. Keep the existing DM policy and retention settings. Do not add--since: it creates a separate checkpoint and cannot initialize the ordinary one.--fullis not needed and would bypass the purge floor. - If the chosen budget is still insufficient, choose a larger budget before
retrying. After a successful sync, restore the original
max_pages. Ordinary sync then starts from the completed MCP history watermark, subject to overlap and retention. A later interval can still exceed that limit and need the same recovery.
This is an operator-managed backfill, not automatic continuation across restarts.
An interrupted scan keeps its pending interval; a retry starts that interval
again. API/Desktop rows, API backfill and --latest-only eligibility cannot
certify the MCP checkpoint. This procedure does not extend the native reference
server's fixed history window or allow text intake with include_dms = false.
A completed empty scan is recorded explicitly and preserves any earlier history
watermark. Later ordinary scans overlap that watermark by one hour. Failed or
incomplete history keeps its original pending interval for retry. Each ordinary
retry reapplies the current purge floor; a prior --full does not grant later
retries permission to restore purged messages. Explicit --since takes precedence
over --full and leaves the ordinary history checkpoint and unselected thread
backlog untouched.
History completion is committed after history writes and before replies. A later reply or channel failure therefore preserves completed history while keeping workspace freshness unchanged. A newer attempt supersedes the older history revision. Checks around each history tools/call discard revoked responses before parsing or further text pagination. Metadata, thread preparation, message batches and later history-derived discovery also check ownership in their write transaction, including empty history outcomes. The older invocation reports a retryable failure instead of admitting those stale writes.
The matching completed revision still permits discovery after history completion. Previously committed batches remain; an in-flight call is not canceled and no new transport retry is added. Workspace/user catalogs, independent queued replies, no-tool tombstone reconciliation and whole-Sync workspace publication after completed history are outside this history-write guard. History progress itself does not advance status freshness.
These local checkpoints are excluded from Git share exports and imports. Merge
preserves the receiver's existing checkpoints; whole-snapshot restore clears them.
Native MCP still makes its existing bounded history request without a wire
oldest argument or cursor pagination. Pending intervals cannot recover messages
outside the server's available window; use API backfill where supported.
Retained MCP threads
With a thread tool, ordinary MCP sync and --full without --since save
eligible retained roots before history writes can remove their reply hints.
New hints and work identified by archived replies are saved with their message
batch, even if a later batch fails. If admitted history revives a parent after
its work was canceled, that transaction saves a new job without replacing any
existing generation, including work created after this sync prepared. Ordinary
sync drains only jobs it prepared or newly queued; another sync keeps its job.
The local replies queue survives request failures, incomplete native replies and
process restarts; it does not store a server cursor or extend the native server's
history window.
Explicit --since, including --full --since, fetches threads only for roots
themselves returned in history. A returned root can qualify through an archived
child even when its reply count is absent. A returned child alone does not add
an older, unreturned parent. After history writes, one transaction checks the
current history revision and acquires only eligible returned roots, using final
stored evidence and positive page hints. It renews selected existing MCP jobs;
unselected backlog and API jobs/skips remain untouched. The shared generation
fences older ordinary or scoped replies, including another Since or adapter.
Channel selection and DM admission still determine which conversations can be read.
Since selects roots; their reply requests have no date bound.
During ordinary sync, a connector without a thread tool reconciles selected stored tombstones after valid history writes, including tombstones merged from a share. This cleanup neither creates nor renews jobs. If live work remains, sync returns an actionable error, preserves the old successful workspace record and requires a connector with thread support before retrying. With no pending work, the existing behavior remains. Explicit Since without a thread tool does not acquire replies work or inspect/clean the ordinary queue.
Every reply traversal checks its acquired generation and live parent around requests and in parent/reply writes, including empty responses. Deletion or renewal stops stale pagination and discards the response after the in-flight request returns; it does not immediately cancel that request or undo earlier committed history or parent writes. A newer history revision alone does not revoke independently acquired replies. Another writer's renewed work can remain pending even when the current sync records successful freshness.
Complete replies retire their matching job even if the enclosing history is incomplete. History coverage still prevents successful workspace freshness; incomplete replies keep their job. Scoped sync acquires all selected roots before requesting replies, so an earlier failure leaves even unvisited selected jobs for a later ordinary retry. Local tombstone, purge and share lifecycle rules are shared with retained API work. Neither queue certifies complete Slack capture or export safety.
MCP direct-message policy
[sync].include_dms = false applies to both --source mcp and connector.
The text connector cannot establish native conversation types, so this setting
stops it immediately after tool discovery, before data calls or archive writes.
Use --source api or a qualified native reference MCP server for this policy.
Tool names, connector metadata, channel names, ID prefixes, and a private flag
alone do not establish conversation type.
For the native reference adapter, strict exclusion reads the complete available
channel catalog once, requiring ok=true on every page. Explicit IDs require an
exact catalog ID match. Existing selectors and exclusions apply before admission;
all observations for a selected duplicate ID are checked together. Selected
catalog identity is checked before DM classification under every policy: a
present foreign context workspace stops the sync even on a DM or duplicate.
That record cannot supply type evidence for the configured workspace. Native
is_im/is_mpim flags exclude that identity even if other channel flags appear.
Other selected conversations require consistent public/private native type
flags; missing or conflicting evidence stops the sync before archive writes,
including with --latest-only. Fixed counts report skipped DMs. If no
conversations are eligible, workspace/user rows and MCP freshness are untouched,
and output says so.
Under every policy, available observations for selected IDs survive name matching and deduplication, including observations from earlier name requests. An excluded alias excludes its whole ID before identity checks; unrelated catalog IDs are not admitted or validated. Omitted/true retain existing payload selection and per-name requests. Direct-ID-only selection still avoids enumeration under those policies, so it does not acquire catalog evidence.
Under every policy, selected catalog and retained message context workspace IDs must agree with the configured workspace. Every returned channel page and thread parent/reply must match the requested conversation and thread before the affected writes. Native message identities are checked before local timestamp filtering or conversion. Top-level history messages require finite numeric timestamps; thread messages require nonblank timestamps, and replies must not reuse the parent timestamp. Nested metadata and catalog latest timestamps remain optional. Earlier successful history writes remain if a later thread fails. Missing context is bound to the operator's configured workspace; it is not authenticated identity proof. External authors and their team IDs are allowed.
Omitted/true retain the existing request patterns and stored payload projections, subject to the identity checks above. Previously archived DMs are not purged. The reference server's bounded history window and MCP cursor-based coverage limits remain unchanged; admission does not certify complete history, DM-origin history, or a safe export.
Ctrl-C cancels MCP sync and stops its stdio server, including when the server stops reading requests and fills the stdin pipe.
MCP response failures report the operation, HTTP status or JSON-RPC code when available, and a fixed reason. Ordinary errors omit server response bodies, tool names and error text, parser snippets, conversation identifiers, and opaque cursors. Transport errors retain cancellation/deadline detection and the credential-origin rejection reason without printing returned URLs. These diagnostics do not establish conversation scope or remove previously archived data.
External Archive Providers
Use [[providers]] to adapt another local archive to the canonical SQLite
schema without adding a source-specific integration to slacrawl itself.
Each provider is an explicit sync source and is not included in --source all.
With [sync].include_dms = false, provider v1 sync stops before reading its
checkpoint or launching the adapter. The protocol accepts arbitrary channel
kinds and opaque raw payloads, so it cannot establish DM exclusion. Use API sync
or a supported Slack workspace JSON export with this setting. Omitted/true keep
existing provider behavior, request bytes, and checkpoint scopes.
This gate governs provider intake. The CLI may initialize the archive and check
Git-share freshness first; earlier configuration/share errors keep precedence.
It does not purge existing DMs or certify an archive as safe to export.
slack.desktop.include_drafts remains specific to Desktop intake.
[[providers]]
name = "archive"
command = "/usr/local/bin/archive-provider"
args = ["provide", "--format", "jsonl"]
env_allowlist = ["ARCHIVE_DB_PATH"]
source_rank = 5
batch_size = 1000
slacrawl sync --source provider:archive --workspace T01234567
slacrawl sync --source provider:archive --workspace T01234567 --channels C01234567
slacrawl sync --source provider:archive --workspace T01234567 --full
slacrawl sync --source provider:archive --workspace T01234567 --limit 100
Configuration rules:
nameis normalized to lowercase and must be unique; whitespace, slashes, and colons are rejectedcommandis required;~or a leading~/is expanded, then the path must be absoluteargsare passed directly to the executable without shell parsingenv_allowlistnames additional variables to copy from the parent environment; values do not belong in TOML- the subprocess otherwise receives only ordinary runtime variables such as
HOME,PATH, temporary-directory, user, shell, and platform variables when present source_rankmust be greater than2; lower numeric ranks win, so provider messages cannot replace canonical rank1or2data; equal ranks may replacebatch_sizedefaults to1000, accepts1through100000, and is forwarded to the provider as a preferred upstream batch size
JSONL protocol
slacrawl writes exactly one JSON object followed by a newline to provider stdin,
then closes stdin. Example:
{"type":"request","protocol":"slacrawl-provider-v1","workspace_id":"T01234567","channels":["C01234567"],"checkpoint":"opaque-provider-state","batch_size":1000,"limit":100}
limit is omitted when no positive limit was requested. The other optional
fields may also be omitted when empty. checkpoint is an opaque string that
the provider previously emitted; slacrawl never parses it.
Provider stdout is JSONL. Records must appear in this order:
- One
hellorecord before any other output. - Zero or more data and checkpoint records.
- Exactly one terminal
donerecord, followed by EOF.
An example response looks like:
{"type":"hello","protocol":"slacrawl-provider-v1","provider":"archive-cache"}
{"type":"workspace","id":"T01234567","name":"Example Workspace","raw_json":{}}
{"type":"user","workspace_id":"T01234567","id":"U01234567","name":"example","raw_json":{}}
{"type":"channel","workspace_id":"T01234567","id":"C01234567","name":"general","kind":"public_channel","raw_json":{}}
{"type":"message","workspace_id":"T01234567","channel_id":"C01234567","ts":"1772574099.659199","user_id":"U01234567","thread_ts":"","text":"Example message","raw_json":{}}
{"type":"checkpoint","entity_type":"workspace","entity_id":"T01234567","value":"opaque-next-state"}
{"type":"done","records":4}
done.records counts workspace, channel, user, and message records; it
does not count hello, checkpoint, or done. Optional fields by record type:
workspace:name,domain,enterprise_id,raw_jsonchannel:name,kind,topic,purpose,is_private,is_archived,is_shared,is_general,raw_jsonuser:name,real_name,display_name,title,is_bot,is_deleted,raw_jsonmessage:user_id,thread_ts,text,raw_jsondone:skipped_users,skipped_channels,skipped_bad_message_id,skipped_duplicate_identity,skipped_missing_channel,skipped_missing_timestamp_sort_key,rounded_thread_messages,rounded_threads,unresolved_thread_messages, andunresolved_threads
Unknown JSON fields are ignored.
Required identity fields are the requested workspace id; channel and user
workspace_id plus id; and message workspace_id, channel_id, and Slack
ts. A message channel must already exist in the database or have appeared
earlier in the stream. An unknown nonempty message user ID is preserved as a
sparse workspace-bound profile that a later real user record may enrich. Empty
names fall back to their IDs, empty channel kinds become provider_channel,
and missing or null raw_json becomes {}.
slacrawl builds repaired, normalized search text from each message, extracts
mentions, updates FTS, and uses source_rank reconciliation for canonical
message rows. Metadata emitted by a provider is sparse enrichment and does not
replace richer existing workspace, channel, or user metadata.
Checkpoints, scopes, and limits
The normal unfiltered incremental run stores one checkpoint under
provider:<name> and the workspace ID. Any --since, --full,
--latest-only, channel filter, exclusion, or --limit gets a deterministic
scope-specific checkpoint. This prevents a bounded validation import or a
partial backfill from advancing the normal incremental cursor.
A checkpoint record flushes pending records and the checkpoint in one SQLite
transaction. Emit it only after all records covered by its value. If the
provider later exits nonzero or omits done, the command fails, but already
committed records and checkpoints remain available for the next retry. A
successful unbounded --full promotes its last checkpoint to the matching
incremental scope; a limited full run does not.
--limit is intended for validation imports. slacrawl forwards a positive
message limit and fails if the provider emits more messages. The limit applies
only to message records, not catalog records. Providers must also interpret
and honor since, full, latest_only, channels, and exclude_channels;
slacrawl forwards those values but cannot infer source-specific filtering.
Incremental provider imports enforce stored channel retention floors. --full,
or an explicit --since older than a channel's floor, is treated as a deliberate
restore and may reintroduce purged history.
Safety and failure behavior
- treat the executable as trusted code inside the archive visibility boundary
slacrawlrestricts environment forwarding but does not sandbox the executable; it retains the caller's filesystem and network permissions- allowlist only variables the provider needs, especially credentials or archive paths
- keep separate configs and databases for archives with different visibility
- the first record must be
hellowith protocolslacrawl-provider-v1 - cross-workspace records, missing required identities or channels, protocol mismatches, output after
done, and record-count mismatches fail closed - the process must emit
done, close stdout, and exit zero for the run to succeed - EOF without
doneflushes pending records before reporting failure; exceeding--limitlikewise flushes pending records through the limit before failing - provider stderr is capped and attached to failures for diagnostics
- provider v1 transfers metadata and message text, not file blobs
Git Archive Sharing
Use [share] when you want one machine to publish a private Slack archive snapshot and other machines to query it locally without Slack API credentials.
[share]
remote = "git@github.com:your-org/private-slacrawl-archive.git"
repo_path = "~/.slacrawl/share"
branch = "main"
auto_update = true
stale_after = "15m"
Behavior:
publishexports gzipped JSONL table shards plusmanifest.jsonintorepo_path- snapshots include eligible cached public-channel media by default; table shards are gzip-compressed and media files retain their raw cache layout
subscribewrites a git-reader config, disables Slack API and desktop sources for that config, clones the repo, and imports the snapshot- pass
--dbtosubscribewhen you want the reader archive to use a non-default SQLite file updatepulls and merges changed snapshots; an unchanged manifest can still restore missing media and refresh successful-import statesearch,messages,mentions,sql,users,channels,report,digest,analytics, andfilesauto-refresh stale git-backed snapshots before querying whenauto_update = truestale_aftercontrols how old the last successful import can be before the next read pulls/imports againstatusanddoctoronly observe the configured share repo and last import / manifest freshness; they do not auto-refresh
With [sync].include_dms = false, legacy Git snapshot imports stop because
their raw table format cannot enforce DM exclusion. This covers subscribe,
update, exact/historical restore, and stale automatic imports before read,
non-Desktop sync, tail, or file-fetch commands. Subscribe rejects before saving
its importing configuration; explicit updates reject before opening the archive
or acquiring Git data. Automatic paths open the archive to check staleness,
then reject before acquisition, snapshot/media import, or freshness updates.
Set [share].auto_update = false to continue querying the local archive or
syncing API/native sources while retaining DM exclusion. Fresh automatic reads,
subscribe --no-import, and Desktop-only sync/watch remain available.
Omitted/true keeps the existing merge and restore behavior.
Explicit [sync].include_dms = false also blocks publish, including
--no-commit, --no-media, and tag/push modes. After flag, config and argument
checks, publishing rejects before archive initialization, media-cache locking,
Git work or snapshot writes. Keep the archive local. Omitted/true preserves
unfiltered private snapshot publishing, including archived DMs and drafts;
intake settings do not purge those rows or certify publication safety. This
gate does not change the optional release notifier before command dispatch.
Token Sources
Each Slack token source is controlled independently.
Text normalization notes:
- malformed UTF-8 is repaired before indexing
- compatibility forms are normalized with NFKC
- zero-width and non-printable control noise is stripped from indexed text
- weird spacing is collapsed so FTS and mentions stay queryable even when Slack/Desktop payloads are messy
Bot token
When configured, the bot token is primary for API sync:
- channel discovery
- users snapshot
- channel history
An invalid configured bot stops sync; it does not fall back to a user token. Disable it for user-only API sync or desktop-only operation:
[slack.bot]
enabled = false
token_env = "SLACK_BOT_TOKEN"
App token
Use the app token with a bot token for live Socket Mode tailing. A user token plus an app token does not satisfy Tail's bot requirement:
[slack.app]
enabled = true
token_env = "SLACK_APP_TOKEN"
If app tailing is not needed, disable it:
[slack.app]
enabled = false
token_env = "SLACK_APP_TOKEN"
User token
Without a configured bot, API sync authenticates the user as primary for
workspace validation, channel discovery, profiles and history. This applies to
api, its bot alias, and the API portion of all. User-primary history uses
api-user rank 1, never joins channels, and reports ordinary history scope
errors. Its history coverage and successful workspace marker remain separate
from api-bot; switching tokens does not borrow the bot's completed interval.
With a bot, the user is optional. Slacrawl uses it for historical replies and optional DM discovery while the bot retains primary ownership.
When both authenticate, bot and user tokens must belong to the same workspace. API sync and periodic tail repair reject a valid user token from another workspace before fetching data or updating the archive. Doctor reports that mismatch as unavailable user auth with partial thread coverage. Missing or invalid user tokens retain bot-only coverage when a bot is primary. User-primary auth errors and cancellation stop Sync. CLI archive initialization and automatic share checks still occur before API authentication.
[slack.user]
enabled = true
token_env = "SLACK_USER_TOKEN"
If you do not want user-token access at all:
[slack.user]
enabled = false
token_env = "SLACK_USER_TOKEN"
Doctor coverage ownership
Doctor authenticates a configured user even without a bot and fills the existing user availability/error fields. Bot credentials remain absent and Tail remains unavailable without them. Failed user auth is reported as an unavailable capability rather than a successful user session.
In JSON, slack_api.thread_coverage describes global credentials; top-level
thread_coverage uses the named-workspace aggregate when present. An archived
api-user/thread_skip, pending API thread work or incomplete retained API
history downgrades either full result to partial in both fields.
Individual workspace_api diagnostics remain unchanged. Doctor obtains its
Status projection and explicit retained facts from the same archive snapshot;
neither read rewrites the historical coverage marker. Recent channel skips
combine api-bot and api-user, newest first with channel-ID tie ordering and
one limit of 20.
If retained API work changes global coverage from full to partial, Doctor adds
slack_api.thread_coverage_reason = "retained_api_thread_work" and displays
partial: retained API thread skips or pending work. History-only incompleteness
uses retained_api_history_work with partial: incomplete API history; thread
skips/jobs take precedence when both exist. It does not describe a valid user
session as missing. An already-partial global auth result keeps the
authentication explanation, even when retained work also lowers the named
aggregate. JSON omits an unset reason; --format log includes
thread_coverage_reason="-". With valid user auth and no recognized reason,
human output reports partial without guessing the cause.
Doctor DM access sampling
Doctor samples at most one available IM and one MPIM history. A non-scope catalog
failure sets optional dm_probe_error = "catalog_failed"; a non-scope history failure
sets "history_failed" and does not prevent sampling the other kind. Missing
scopes accumulate independently, so scope warnings and a probe failure can appear
together. Global and named workspace output reports both, with a retry suggestion.
The new field contains only those codes, never provider payloads or cursors.
JSON omits an unset field; log output displays dm_probe_error="-".
These checks leave authentication, DM inclusion and thread-coverage decisions
unchanged. user auth available for replies describes a capability, not a
completed backfill. enabled for user token; history coverage not verified does
not claim that empty catalogs or unsampled kinds passed history checks.
Canceling Doctor or reaching its caller deadline stops subsequent requests and
returns an error without a report. A request timeout while the caller remains
active is a bounded probe failure; unavailable optional auth keeps its existing
fallback behavior.
Retained API threads
Ordinary API sync and --full without --since revisit eligible roots already
in the selected channels, even when fetched history no longer contains their
reply hints. Roots need a positive reply count or a distinct archived child;
an empty or self-referencing thread_ts is supported. Explicit --since,
--full --since and Tail repair do not drain this backlog. A committed replies
collision queues or renews its requested owned/live root for ordinary retry;
excluded conversations leave work untouched. Current-page thread processing
keeps its existing scope.
Replies work is saved locally before history can overwrite hints and in the same transaction as fetched messages that establish roots through reply counts or retained child relationships. Later history errors, incomplete responses and intentional scope skips keep that work pending for a later sync. When user replies are unavailable, bot history can still complete with partial thread coverage; the saved work remains. Switching from bot-primary to user-primary sync keeps that replies work without borrowing the bot's history checkpoint.
Existing message reconciliation can revive a tombstone when an in-flight history page arrives later. If that page admits thread evidence after cancellation, its transaction queues fresh work while preserving every existing generation, including work another sync created after this sync prepared. Ordinary replies requests use only generations prepared or newly queued by this invocation; a competing sync keeps ownership of its response and skip state across those history commits. Roots successfully completed during this sync stay excluded. If a generation is rejected before any replies request or committed cached skip, a later page can admit and process fresh work. Revocation after a request still counts as an attempt and can leave newly queued work for the next sync.
Starting a new ordinary preparation renews selected retained generations, even without replies capability, and can supersede an earlier replies worker. The replacement work remains pending with partial coverage. Capability-aware thread preparation and concurrent-sync fairness remain follow-up work.
Successful replies retire only the generation that was processed. Retained
requests and writes recheck that generation and the parent's live ownership;
deletion or renewal during a request discards its stale response. Bulk retirement
of unrelated legacy thread skips requires Full with no --since, no channel
allow-list and no effective channel exclusions, after DM enumeration completes
with DMs enabled and no conversation or message work omitted. Full cleanup also
keeps thread-skip records while API replies work or incomplete API history in
that workspace remains. The history check and pending-thread deletion guard share
one writer transaction; canonical foreign-workspace records do not block cleanup.
Successful individual replies still clear their own skip during scoped runs. Remaining retained work runs after complete history traversal
and before the completed history horizon is saved. A replies failure can therefore
stop later channel or media work while preserving committed messages and the
pending history interval.
DM catalog filtering or missing scope, admission drops, recoverable history skips, unavailable retained roots, and channel or message ownership collisions keep this sync's recorded thread coverage partial. A later successful channel or concurrent thread completion does not erase that omission. Scoped runs retain unvisited diagnostics; successful scoped replies still clear their own skips. History or replies collisions keep that channel's exact attempted interval and prior completed horizon. A collision anywhere in replies pagination also preserves the job and exact skip; later errors still win, and valid rows on other pages or channels commit. With no owned generation, that collision queues or renews only the requested live root, even without reply-count/child evidence. The queue write follows discovery filtering and commits with the page, so an older ordinary reply cannot retire the new generation. An ordinary retry can drain it without fresh history hints; successful scoped replies leave the job.
Catalog-only omissions still belong to the current invocation. CLI Doctor reads retained history/work when evaluating full coverage; its store-free capability probe does not certify archived completeness. Static scope restrictions alone retain the existing scoped coverage behavior. No erased evidence is reconstructed, and this does not qualify mixed older writers or continuously current raw coverage markers.
When a retained root returns thread_not_found, its job remains pending with
a root-specific skip and partial coverage. Healthy roots and later channels
continue, and the next ordinary sync retries the unavailable root. The error
does not establish deletion. Other reply failures retain their existing behavior.
Committed parent tombstones, message removal through the store, and local purge cancel matching work and its API thread-skip record, even when the pending job is already absent. Ordinary preparation also reconciles skips backed by stored tombstones in the selected workspace and channel, including after a Git-share merge and without a remaining reply hint. Live or missing parents are not deletion evidence; malformed pending work still requires repair. History polling does not discover hidden deletion events; see Slack's message contract. Missing reply metadata is not deletion. The local jobs are excluded from Git share export/import and freshness timestamps; archive source entry counts still include them. A full restore clears local work along with replaced archive rows. MCP uses the same local work lifecycle with its own reply and scope rules. This does not certify complete Slack capture.
API catalog page success
Channel and user catalogs must report ok: true before their rows or cursors
are returned. Missing, null or false success with no concrete Slack error stops
that catalog operation. A failed later page discards its earlier catalog pages;
retry starts the catalog again. Already completed public history survives a later
user or DM catalog failure, while final workspace/Doctor markers and unvisited
legacy thread skips stay unchanged. Existing concrete missing-scope handling
keeps its current skip behavior.
Ordinary catalogs and profiles use the selected primary token. DM discovery uses the user token; periodic repair retains the bot-owned channel catalog. Empty successful catalogs remain valid, including the existing second user fetch when enabled-DM sync receives an empty user result.
Catalog responses now use the same whole-body reader as history/replies, rejecting
trailing JSON and read errors after a valid object. Non-200 responses use SDK
status errors; bounded retries remain limited to rate limits with Retry-After.
Successful catalog pages must also contain a channels or members array.
Missing/null collections leave the page uncertified; nonarrays fail decoding.
Explicit [] remains valid, including on pages with a continuation cursor. This
archive-completeness rule does not label all omitted/null Slack responses invalid
or certify the rest of their payload shape. Doctor probe-error handling is unchanged.
API authentication, lookups and joins
auth.test, conversations.info and conversations.join must report ok: true
before slacrawl uses their decoded results. Missing, null or false success with
no concrete Slack error now fails with a method-specific error. These methods
also reject trailing JSON and read errors after an otherwise valid object.
Concrete Slack errors, typed HTTP status errors and bounded rate-limit retries
keep their existing handling. Success-flag validation does not certify workspace
identity or the rest of the payload shape.
A failed primary authentication stops sync before archive writes; a configured bot never falls back to the user token on failure. Failed optional user auth still allows bot history, with replies unavailable. Doctor returns bot-auth failures and reports user-auth failures as unavailable. Tail rejects failed bot auth before starting Socket Mode, and an unsuccessful lookup for an untyped event stops tailing without writes or acknowledgement. A failed join remains a recorded, nonfatal history skip: sync can finish with partial coverage, but does not retry history as though the join succeeded. Doctor reports non-scope capability-probe failures separately from missing scopes.
Workspace-bound operations require auth.test to identify a nonblank workspace
after trimming whitespace. A configured or requested workspace cannot replace
that identity. A successful response with an unbound primary identity stops sync
or Tail; an unbound optional user identity stops sync and repair before writes.
Concrete optional-user authentication errors still allow bot-only history.
Doctor rejects an unbound bot identity, reports an unbound user as unavailable,
and uses the canonical user workspace ID for DM probing.
Use a workspace-scoped bot or user token. An enterprise ID may accompany a valid
workspace ID but cannot replace it; organization-token workspace resolution is
not supported. The existing users.list request leaves team_id empty.
API request diagnostics
Native request/response failures report a fixed operation and phase, with numeric HTTP status when relevant. Endpoint URLs, transport/read error text, invalid Retry-After values and SDK decode snippets are omitted from these rendered errors. This also protects optional-auth Doctor JSON, failed-join state and progress logs when they report these failures. Existing retry and partial-work behavior stays unchanged; the diagnostic no longer includes the underlying failure detail.
For decoded unsuccessful responses, these exact native codes remain readable:
missing_scope, not_in_channel, channel_not_found, invalid_auth,
not_authed, account_inactive, token_expired, token_revoked,
is_archived and thread_not_found. Other error strings produce slack <method> API response failed;
whitespace, case changes or added text do not qualify. Repeated channel, DM, user,
history and replies cursors report the method without reflecting the cursor.
The SDK's explicit-success error-field bypass, exact skip classification, retries
and pending-work behavior remain unchanged.
Causes remain available to code using errors.Is, errors.As or unwrapping and
may still contain private native codes, details or metadata. This is not redaction
of error objects, identities, progress names, successful metadata or archive data,
previously stored diagnostics, or every diagnostic surface.
API history completeness
Native history and replies pages must report ok: true before any message on
that page is admitted. Missing, null or false success with blank or absent error
text stops sync or repair with a method-specific error. Concrete Slack errors
keep their existing type and details; previously committed pages survive, and
the pending interval remains available for a corrected retry.
Successful history/replies pages must contain a messages array. Missing/null
collections leave the page uncertified instead of completing an empty interval;
explicit [] remains valid. Earlier writes, completed horizons and pending work
retain their existing retry behavior. The array requirement also applies to
one-message capability probes; Doctor reports non-scope probe failures separately
from missing scopes. Those diagnostics do not certify complete history.
Authentication, conversation-info and join responses are outside this page-only
check.
API sync and periodic tail repair follow every nonempty history/replies cursor,
even when a page is short or empty. After writing a valid page and handling its
scheduled threads, a terminal has_more = true without a continuation cursor
stops the scan with an error. slacrawl does not support timestamp pagination
to continue without a cursor. Unchanged responses leave the same interval
pending on retry.
History is_limited = true is retained across accessible pages. Once those
pages have been traversed, the scan reports that completeness of the requested
interval is uncertified. Slack defines this flag for earlier messages beyond
the free-workspace message limit. It does not establish that a particular
bounded interval is missing messages or detect every access/retention limit;
narrowing --since is not a completeness bypass.
These checks apply under every DM policy. Valid fetched rows remain stored,
but the previous coverage Latest and Complete stay unchanged, the attempted
lower bound remains in Pending, and ordinary workspace success does not
advance. A corrected retry resumes that pending interval and can complete it.
Concrete request, validation, write, and cancellation failures keep their
precedence; native transport/decode failures use the bounded diagnostics above.
One-message capability probes require successful page responses but remain
independent of scan completion. Doctor reports non-scope probe failures without
changing authentication or thread-coverage decisions.
See Slack's history contract, replies pagination, and cursor pagination guidance.
API and tail direct-message policy
For API sync, [sync].include_dms defaults to whether a user token is configured.
Set it to false to exclude IM and MPIM conversations before channel metadata,
history checkpoints, or messages are written.
Explicit is_im/is_mpim flags take exclusion precedence even when other
channel flags are present.
For other selected conversations, missing or conflicting native channel flags
fail the sync instead of being treated as complete.
API sync channel allow-lists and excluded names still apply before this check.
Periodic tail repair lists public/private channels and shares this explicit
exclusion check, but does not discover DMs or carry API sync channel selectors.
All API policies reject missing conversation IDs, conflicting context workspace IDs, and mismatched channel IDs in typed latest-message, history, or reply payloads. A rejected page is not written; earlier pages and unfinished coverage remain available for retry. Slack Connect authors and conversation hosts may belong to other workspaces and do not trigger this check.
API history/replies and periodic repair also reject empty or whitespace-only top-level message timestamps before writing any part of that page. Accepted timestamps stay unchanged; nested metadata and catalog latest timestamps remain optional, and native replies may echo their parent. Failure preserves earlier pages and the previous successful workspace state; retry resumes the pending history interval.
For live Socket Mode events, omitted/true keeps accepting delivered DMs even
without a user token. Explicit false skips im, mpim, and the retired
workspace-app app_home DM event type before content normalization or writes,
then acknowledges the intentional skip. Native channel and group message
events need no extra request. Missing or unrecognized types, including ordinary
edit/delete envelopes, and rename/archive/unarchive events require a fresh
conversations.info
lookup with the bot token. Grant the relevant channels:read, groups:read,
im:read, or mpim:read access. Lookup failures and identity mismatches stop
tailing without acknowledging that event. After identity validation, explicit
is_im/is_mpim flags cause an intentional skip and ACK even when other channel
flags are present. Otherwise, unknown/conflicting channel flags stop tailing
without ACK; correct the access/configuration problem before restarting.
These lookups run before ACK and use the existing rate-limit retry behavior. They can delay ACKs beyond Slack's delivery deadline; no prompt-ACK guarantee is made for events requiring lookup. Results are not cached, and the returned conversation and its latest message are not stored. Channel metadata updates only affect existing rows in the authenticated workspace; missing or foreign rows are intentional no-ops under every policy. Retained message channel IDs must agree with the envelope, including nested edited/deleted/root messages. Slack Connect event/author workspace IDs and differing event/message timestamps are not conversation identity conflicts.
This controls future API/tail/desktop/MCP and Slack-export intake and rejects legacy Git share imports and external provider v1 sync. It does not purge archived DMs. Desktop uses the policy as described below; MCP uses the native evidence requirements above. It does not certify the archive or a Git share as safe to publish: admitted messages can contain sensitive text and file metadata, and a current channel type does not establish that its history lacks messages from a converted group DM.
Slack Export Admission
[sync].include_dms = false applies to import, including --dry-run and --force.
Omitted/true preserve DM inclusion. Strict admission supports a declared Slack
workspace JSON export with channels.json, groups.json, dms.json and/or
mpims.json reference catalogs. A present empty array is valid; null catalogs are invalid. Unrelated
directories/ZIPs without those catalogs are unsupported. Single-user/TXT and
other layouts need separate qualification.
DM catalogs or positive native is_im/is_mpim observations exclude every
occurrence of the same conversation ID. Their message bodies are not decoded.
For non-DM catalogs, if all four discriminator keys (is_channel, is_group,
is_im, is_mpim) are absent, the catalog supplies public/private type and any
supplied is_private must agree. If any discriminator is present, sparse
fallback is disabled: positive native channel flags must establish a public or
private channel. Catalog privacy fills only an absent is_private; explicit
native privacy wins. Negative-only, all-false, contradictory, null or malformed
flags cannot establish admission. Recognized native flag names are matched
case-insensitively for classification and the DM veto. Names and ID prefixes
never establish type.
Strict catalogs and admitted message JSON reject repeated decoded object keys before projection, including nested objects and escaped duplicate keys. Catalog ID/name and native type/privacy keys also reject case-insensitive duplicates. Omitted/true keep the existing JSON decoder behavior.
All catalog IDs and name/ID candidates remain reserved, including excluded and unused fallback candidates. Directory imports compare device/inode identity on Linux and macOS; strict directory admission on other platforms requires a ZIP instead. Contained aliases are allowed only for the same conversation. ZIPs retain original entries and frozen local-header metadata and reject ambiguous logical names or cross-conversation payload reuse. Payloads still use the standard ZIP checksum/decompression path.
Retained raw conversation IDs and context_team_id must match their prepared
channel/workspace before projection or timestamp/priority skips. This includes
every case-insensitive occurrence of recognized identity fields and message,
previous_message, root, previous and catalog-only latest objects. External author teams, mentions and file-sharing references are not
enclosing conversation identity. Missing channel identity inherits the catalog.
After body validation, imports check existing workspace ownership for admitted channel IDs and every retained user ID before workspace, user or channel writes. Dry-run performs the same checks. Store write-time checks remain in place; the preflight does not make concurrent imports or the whole import atomic.
The first body scan fixes the name-first/ID-fallback choice and records each file's identity and digest. Any raw row, even one without a timestamp, selects the name branch; only zero rows permit ID fallback. The write pass verifies and decodes the same opened-file buffer. New files/catalog edits wait for a new preparation; missing, replaced or changed planned files stop execution. Earlier 500-message commits survive a later failure; the pending remainder does not commit.
Strict all-excluded imports do not initialize the archive/runtime directories or write workspace/users. Dry-run opens an existing database read-only, treats a missing one as empty, and never migrates or repairs it. Read-only errors stay visible. These promises concern importer archive/runtime initialization, not the CLI's independent interactive release notice or SQLite WAL sidecar bytes.
Fixed omission counts report excluded conversations. Already archived rows remain, independent user profiles remain eligible, and import retention/priority semantics are unchanged. Catalog evidence does not authenticate the producer, prove complete capture, or establish that a current channel never originated as a group DM. This is not a public-export minimizer or a safe-to-publish verdict.
Desktop Source
Desktop ingestion is optional and read-only.
[sync].include_dms = false also gates desktop conversation-derived writes.
Omitted/true preserve desktop recovery defaults. Strict exclusion requires
selected public/private channel metadata and omits unknown, conflicting, or
heuristic records; every draft destination must be eligible. Fixed omission
counts explain partial intake. Existing archived rows, independent profiles,
and custom statuses remain. See Desktop DM exclusion
for classification, decode coverage, and retained-history limits.
Sent Redux messages honor current purge retention floors at batch write time across desktop/wiretap, watch, and all/hybrid. Metadata and admission counts may still refresh; drafts have separate retention behavior. See Retention after purge.
New read-marker checkpoints use workspace/channel tuple keys in
desktop/read_marker_v1; legacy channel-only rows remain untouched.
See Read-marker checkpoint identity
for scalar values, user/call aggregation and snapshot compatibility.
[slack.desktop]
enabled = true
path = ""
Behavior:
enabled = trueturns on desktop sync supportpath = ""auto-detects the supported macOS or Linux Slack Desktop pathpath = "/custom/path"overrides detectioninclude_draftsdefaults totrue; set it tofalseto exclude unsent drafts from desktop/wiretap sync,watch, and the desktop phase ofsync --source allorhybrid
[slack.desktop]
enabled = true
include_drafts = false
Excluded drafts do not create messages, channel hints, event history, search entries, or draft counts in sync output. Desktop snapshots still read the local cache. This setting does not delete drafts already archived, filter DMs, or sanitize Git snapshots; use a separate empty database when starting an archive that must never contain drafts.
To disable desktop ingestion completely:
[slack.desktop]
enabled = false
path = ""
Sync Settings
repair_every
Used by tail to run periodic API reconciliation during live sync.
[sync]
repair_every = "30m"
desktop_refresh_every
Used by watch to periodically refresh local Slack Desktop state into SQLite.
[sync]
desktop_refresh_every = "5m"
concurrency
Used by API sync to fan out channel history fetches across workers. Keep the default unless you have a reason to tune it for a specific workspace.
Notes:
- higher values increase API fan-out, not write parallelism inside SQLite
- useful mainly for multi-channel API sync, not single-channel runs
--concurrencyon the CLI overrides the config value for that run
latest-only
Use sync --latest-only when you want to refresh only channels that already have a stored cursor.
Notes:
- useful for fast publisher jobs that already seeded history once
- channels with no local history are skipped instead of triggering a first-time backfill
--fulloverrides this behavior and still does the full crawl
Recommended Profiles
Desktop only
[slack.bot]
enabled = false
token_env = "SLACK_BOT_TOKEN"
[slack.app]
enabled = false
token_env = "SLACK_APP_TOKEN"
[slack.user]
enabled = false
token_env = "SLACK_USER_TOKEN"
[slack.desktop]
enabled = true
path = ""
API sync without live tail
[slack.bot]
enabled = true
token_env = "SLACK_BOT_TOKEN"
[slack.app]
enabled = false
token_env = "SLACK_APP_TOKEN"
[slack.user]
enabled = true
token_env = "SLACK_USER_TOKEN"
API sync with live tail and desktop refresh
[slack.bot]
enabled = true
token_env = "SLACK_BOT_TOKEN"
[slack.app]
enabled = true
token_env = "SLACK_APP_TOKEN"
[slack.user]
enabled = true
token_env = "SLACK_USER_TOKEN"
[slack.desktop]
enabled = true
path = ""
[sync]
repair_every = "30m"
desktop_refresh_every = "5m"