TrenyxStart
Menu

← trenyx.io / reads

verify-009 · team workspace / Rust · blind read, static · maintainer-fixed

macro: channel access lingered thirty seconds after removal. Cut to ten the next day.

A pre-registered, blind read of macro-inc/macro, a team workspace that puts chat, mail, documents, calls, a CRM and AI agents behind one permission model. Free and unsolicited. Verdict: six of eight claims held with the mechanism cited, one is documented behaviour, one could not be settled from the code, and one security finding, reported privately, narrowed by the team the next day and shipped to production two days after the report.

Blind read · pinned commit 46e8be7c · read 2026-09-15 · reported 2026-09-15 · fix merged 2026-09-16 · live in production 2026-09-17 · re-checked 2026-09-17

One permission model, many doors. Macro's README promises that anything mentioned in a channel is shared with its members, that leaving a channel loses that access, that threads carry their own permissions, that agents act as the person who invoked them, and that the MCP surface covers nearly everything the UI can do. Thirty-four of the last hundred commits carry a model co-author trailer, Cursor mostly. The repository is a Rust monorepo of about 4,300 source files across 204 crates and 47 services, with a real suite of roughly 9,100 tests in 799 files. So the read went to the shared mechanisms: where access is decided for each kind of entity, how the MCP proxy authenticates, who an agent runs as, and where tokens and memory live.

result
pre-registered claims reached a cited verdict7 / 8 (one left unclear, stated below)
held6 (live channel-membership checks, thread access gated by the channel extractor, per-user JWT on every MCP request through the same access service, per-user memory, envelope-encrypted provider tokens, scheduled actions run as their server-set owner)
documented behaviour1 (team-wide CRM email sharing when Email Sync is on; a note went with the report on its default and its join-time backfill)
security findings1, medium-high as pre-committed
maintainer replywithin a day; fix merged the next day; live in production two days after the report

What held is most of the story. Channel membership itself is checked live on every request, with no cache. Replies in a thread sit behind the same channel-role extractor that gates the channel. The MCP proxy validates a Macro-issued, user-bound token on each call and builds its tools on the same access service the API uses, so there is no shared MCP credential to steal. Memory is generated and stored per user; nothing in the repository builds a team-wide memory document. Microsoft refresh tokens are stored under envelope encryption with a per-row data key, nonce and KMS key id, all required by the schema. A scheduled action's owner is set by the server from the authenticated creator and cannot be changed by the client.

The one claim the read could not settle is zero data retention with model providers. Macro's own OpenAI and Anthropic clients set no retention flag, the OpenAI path is built through an external crate whose defaults are out of scope, and no file in the repository states that retention is handled contractually. The record says unclear, which is the honest verdict when the answer lives in a contract rather than in code.

The finding sat one layer below the live check. Membership is live, but the set of things a member can reach through a channel, the documents, chats, projects, email threads, calls, agent sessions and initiatives shared into it, was resolved by a function cached per user for thirty seconds, with nothing in the repository invalidating it. Seven access checks consumed that cache, over the API and MCP alike, and the search path carried its own thirty-second copy. So a member removed from a channel kept reading everything shared through it for up to thirty seconds, while the project's own docs promise that removal revokes what was shared. Not a way in for a stranger; a lag in a promise the product makes to the person doing the removing.

A live gate with a cached shadow. The catalogue carries this shape under its own name: the membership check is exact, and the derived-access lookup beside it is served from a timed cache with no invalidation on the event that should clear it. The tell is the pair: one function checked live, its neighbour cached, both feeding the same decision.
What happened next

Reported by email. Narrowed the next day. Live two days later.

The project takes reports at a security address named in its README rather than through GitHub, so the report went by email on the fifteenth, with what held stated first, the finding as a file and line, and the CRM item as a note rather than a finding. Will Hutchinson replied within a day: the team would shorten the cache window and considered a short stale window a reasonable trade-off, and the CRM behaviour was a product decision, which matches how the record already graded it. The change, PR #6427, merged on the sixteenth and is exactly five edits: every access cache, the three in the entity-access queries and the two on the search path, goes from thirty seconds to ten. On the seventeenth he wrote that it was live in production.

I re-checked the main branch that day: all five sites read ten seconds, no thirty-second site remains, and there is still no invalidation on removal. That is the fix as the team defines it, and the record says so: fixed, with the ten-second window noted as the trade-off they chose, so anyone reading it later sees a decision rather than a gap. The read counts as replied and fixed. The team was asked nothing, and this write-up went to them before it went anywhere else.

What this read did not do

It read the code, partially, and never touched the tests.

A blind read is a static pass at one pinned commit: the plan is hashed first, then the code is read against it, and every verdict cites a line. In a repository this size the read is partial by design, and the record lists which crates and services were opened and which were not. It does not build or run the project, and it does not plant anything in the project's suite. The sealed engagement is the other kind of entry on this site: catalogue defects planted one at a time into a clean checkout, the project's own tests run against each, and the escapes counted. The analitiq-engine entry is what that looks like, with its kill count.

The plan for this read was written from the project's README alone, hashed, and timestamped to the Bitcoin blockchain before the code was cloned. The plan stays private while the fault catalogue it draws on does. The findings, the report and the maintainer's response are the receipts on this page.

Engagement repo: private   Read the fix

This is what an engagement looks like

Want your codebase read the same way?

Every engagement ships the pre-registration, the findings with their citations, and the maintainer's response, disclosed to you first, privately, with the same receipts you see here.

How to work with me →