Skip to content

tdc channel threads fails with READ_ONLY for a read-only token, while sibling read commands succeed #51

Description

@domjost

Investigated and written by Claude Code on Dominique Jost's behalf, submitted from his account. Not manually authored by Dominique. (Same convention as #23.)

Summary

tdc channel threads fails with READ_ONLY when the CLI is authenticated in read-only mode, even though listing threads is a read operation and the token carries comms:channels:read and comms:content:read.

It is the only read subcommand that behaves this way. Two siblings in the same command group, channel list and channel members list, both succeed on the same token.

Version and environment

  • tdc 2.0.2 (@doist/comms-cli)
  • macOS (darwin 25.6.0), node v25.2.1

Auth state

$ tdc auth status
✓ Authenticated
  Email: dominique@doist.com
  Mode:  read-only (scope: comms:channels:read comms:content:read comms:messages:read user:read workspaces:read)

Reproduction

$ tdc channel threads id:CcZXsoctdNfxcA5cdKUSB --json
{"error":{"code":"READ_ONLY","message":"This CLI is authenticated in read-only mode. Re-run `tdc auth login` without --read-only to enable write operations.","hints":["Re-run: tdc auth login (without --read-only)"]}}

Fails identically with:

  • a bare channel ref (tdc channel threads CcZXsoctdNfxcA5cdKUSB --json)
  • an explicit workspace positional (tdc channel threads CcZXsoctdNfxcA5cdKUSB 69 --json)
  • --limit 30, --since, and with no flags at all

Expected

A list of threads in the channel. comms:channels:read or comms:content:read should cover it.

Only this one subcommand is affected

Same token, same session:

Command Result
tdc channel threads id:<id> --json READ_ONLY
tdc channel list 69 --json OK (same command group)
tdc channel members list id:<id> --json OK (same command group)
tdc thread view <id> --json OK
tdc inbox --json OK
tdc conversation list 69 --json OK
tdc users 69 --json OK
tdc user --json OK
tdc workspaces --json OK

So this is not the channel group being classified as writes wholesale. Its siblings are fine.

Where the gate might live

The blocked call consistently takes longer than a successful one:

  • channel threads (blocked): 1.35s, 1.43s
  • channel list (succeeds, hits the network): 1.05s

Because the rejection is slower than a successful round trip, it does not look like a local pre-flight check. That points toward the API rejecting the request and the CLI mapping the rejection onto READ_ONLY, rather than a client-side command classification. Not conclusive, since it could also be attempting a token refresh before a local gate, but the scope mapping on the threads-list endpoint seems like the first place to look.

Possibly a regression

In #23 (2026-06-15, tdc 1.5.0), tdc channel threads --json reached the API and failed on a Zod schema validation error for pinned. So at 1.5.0 the command was getting past auth and into response parsing. The READ_ONLY behavior appears to have arrived after that.

Caveat: I do not know whether the token in #23 was read-only, so this establishes that the command reached the API at 1.5.0, not conclusively that read-only tokens were accepted then. The v2.0.0 release notes only mention the node >=24 requirement (#44), so the gate does not appear to have been introduced there.

Impact

There is currently no read-only way to enumerate the threads in a channel from the CLI:

  • tdc inbox covers inbox threads only, not a given channel's threads.
  • The Comms MCP search-content is search rather than enumeration, so it cannot reliably list what is in a channel. (It also returns zero results when its channelIds filter is set, even for queries that match when unfiltered, so client-side filtering is needed.)

This blocks read-only automation that needs to find threads in a channel, for example refreshing per-quarter snippets and HQ thread IDs when squads spin up new threads.

Workaround

Use the Comms MCP search-content without the channelIds filter and filter on channelId client-side. This finds threads by title that tdc search does not return, but it is search, not enumeration, so coverage is not guaranteed.

Possibly unrelated, observed in the same session

tdc search and tdc mentions intermittently return:

{"error":{"code":"INTERNAL_ERROR","message":"fetch failed"}}

Both worked earlier in the same session with the same token, so this reads as transient rather than scope-related. Noted here so it is not lost; happy to split it into its own issue if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions