Codex vs Claude

Starting Work From a Message: Rooms Are Not Authorisation Boundaries

David Guzenburg/ / 11 min read

Your code host knows who may write to a repository. Your chat platform knows who is in a room. Those are not the same fact.

integrationaccess controltoolingaudit

Chat is the loosest authorisation surface you have

Triggering work from a message is the most natural-feeling integration in this whole category. Someone reports a bug in a channel, someone else types a command, and a fix appears. No context switch, no ticket, no tool.

It is also the point at which the weakest identity and authorisation model in your organisation becomes connected to the strongest permissions. Your version control system knows who may write to a repository. Your chat platform knows who is in a room, and rooms routinely contain contractors, customers, partners, people who changed teams two years ago, and a handful of integrations nobody remembers installing.

The gap between those two facts is where every problem in this article lives.

Channel membership is not authorisation

The default implementation of a chat trigger authorises by presence: whoever can post in the channel can invoke the command. That is a permission model with no relationship to what the person may do in the systems the agent touches.

Consider what is ordinary in most workspaces. Channels have guests. Guests are often external. Channels get opened up during an incident and never closed. Someone shares a channel with a partner organisation. Each of those is a routine act with no security review attached, and each of them silently grants whatever the chat trigger grants.

Bind chat identity to code identity explicitly

The fix is a mapping you maintain, not an inference. A table connecting chat user identifiers to code-host accounts, and an authorisation check against the code host's permissions rather than against channel membership.

const actor = await identityMap.get(event.user);       // explicit mapping
if (!actor) {
  return reply("I don't know who you are in our code host. " +
               "Ask an admin to link your account.");
}

const perm = await codeHost.permissionFor(actor.login, repo);
if (!["admin", "maintain", "write"].includes(perm)) {
  return reply(`${actor.login} has ${perm} on ${repo}; this needs write.`);
}

// Only now is it safe to treat the message as a request.
await runTask({ repo, actor: actor.login, brief: event.text });

Unmapped users get nothing. That is deliberate and it is the property that makes the whole integration defensible: a stranger in the channel is not a partially privileged user, they are an unknown one.

The message text is untrusted input

Separately from who sent it, what it says is user-controlled text that will be treated as an instruction. In a channel where anyone can post, that is the same exposure as issue text, with a lower barrier and no audit trail worth the name.

It gets worse with the features chat platforms have. Quoted messages, forwarded messages, link previews that pull in remote content, and integrations that post on behalf of external systems all put text into a channel that no authorised person wrote. An agent reading a thread for context is reading all of it.

Take the brief from the invoker's own message

Do not scoop up thread history as the instruction. Use only the text the authorised person typed in the invoking message, and treat everything else in the thread as material to be quoted rather than obeyed. That one rule removes most of the injection surface.

What chat triggers should be allowed to start

Bounded, recoverable work whose output lands somewhere reviewable. Open a draft pull request. Investigate a failure and report. Summarise what changed in a service this week. Triage an alert. Draft a fix for review.

The common property is that the chat message starts a process which terminates in something a person examines. Chat is the trigger, not the approval, and the actual gate remains where it already was.

What they should not

Deploy. Merge. Run anything against production. Modify infrastructure. Rotate a credential. Anything irreversible, and anything whose consequence exceeds what a mistyped message should be able to cause.

The argument is not that these cannot be done safely from chat — a deploy command with a proper authorisation check and a confirmation is a well-established pattern. It is that they should not be reachable through the agent path, because the agent path interprets natural language, and natural language interpretation is a worse gate than a command parser. "Ship the fix" is ambiguous in a way that deploy service-x v2.3.1 is not.

Ambiguity is the underrated risk

Chat is a casual register, which is why people like it and why it is a poor specification medium. "Can someone fix the thing with the timeouts" is a perfectly normal message and an appalling brief.

An agent will resolve the ambiguity and act on its resolution. The mitigation is to make it restate: the response to an under-specified request should be a proposed interpretation with a confirmation, not an attempt. That costs one exchange and it is the same cheap-veto argument as plan mode, adapted to a medium where the requests are shorter and vaguer.

Chat is ephemeral; the work is not

Messages get edited, deleted, and aged out by retention policy. Channels get archived. So a chat message is a poor place for the only record of why something happened, and it will be the only record unless you arrange otherwise.

Have the agent write the provenance where the work is: the invoking message text, the chat user, the resolved code-host identity and a permalink, all in the pull request description. Then the record survives independently of the chat platform's retention, and a reviewer six months later can see who asked for this and in what words.

Reply where the work is, not where the request was

The natural implementation posts progress and results back into the channel, and that is how these integrations become noise. Ten messages of status for one task, in a channel doing other things, and within a fortnight everyone has muted the bot.

Better: acknowledge once with a link, and put everything substantive in the pull request. One message per task in the channel, and the detail where detail belongs. This is the same discipline as keeping a code-host bot quiet, and it matters more here because chat has no equivalent of a collapsed thread that people can ignore selectively.

Rate limits and cost

A chat command is the cheapest possible way to start expensive work: one line, no ceremony, no sense of having spent anything. That combination produces usage patterns nobody intended, particularly during an incident when several people independently ask for the same investigation.

Per-user and per-channel rate limits, plus deduplication against work already running for the same target, are worth having from the start. So is a visible statement of what a command costs, because the absence of any signal is what makes it feel free.

The audit trail spans two systems

When you need to reconstruct what happened, the request is in one system and the action is in another, and neither is complete alone. Make the join explicit: a correlation identifier present in the chat acknowledgement, the agent's logs, and the resulting pull request.

Without it, answering "who asked for this change" involves searching chat history by approximate timestamp, which works until the message was edited, the channel was archived, or the retention window passed. The general shape of this is in auditing agent tool calls; the chat-specific part is that one end of the trail lives in a system with its own retention policy that you probably did not choose.

The version worth building

An explicit identity mapping, checked against code-host permissions, with unmapped users refused. The brief taken only from the invoking message. Work limited to bounded, recoverable tasks that terminate in a reviewable artefact. Provenance written into the pull request rather than left in chat. One acknowledgement message with a link, and nothing else in the channel. Rate limits per user. A correlation identifier joining the two systems.

That is a day of work and it preserves the thing that made the integration appealing — no context switch to ask for something — while removing the assumption that a room is an authorisation boundary. It never was; it just did not matter until a message could start a build.

Threads are a worse context than they look

A subtlety about using chat as an input medium generally. A thread reads, to a person, as a coherent discussion. To an agent it is a sequence of messages from several authors with different levels of confidence, some of which were corrected later, some of which were jokes, and some of which are the output of another integration.

People are good at weighting those; a summariser is much less good, and the failure is confident synthesis of a conversation that concluded the opposite of what the synthesis says. If you do use thread context, use it to gather material for a human to confirm, not as the specification for work.

Incidents are the stress test

Every property in this article gets worse during an incident. Channels are opened to more people, urgency suppresses confirmation steps, several people ask for overlapping things, and nobody is reading the audit trail.

Which means an incident is exactly when a chat trigger should be most constrained, and it is when people most want it to be permissive. Decide that in advance, while nothing is wrong: what an agent may be asked to do during an incident, by whom, and what stays behind a deliberate non-chat path. A rule made calmly holds better than a judgement made at two in the morning.

The reason to do this at all

Having spent the article on hazards, the upside deserves a fair statement. The friction of leaving a conversation to file a ticket is real and it is why small improvements never get made. A trigger that turns "someone should fix that" into a draft pull request captures work that would otherwise evaporate, and that is a genuine gain in a way that is hard to get any other way.

The point is only that the trigger is a convenience layer over systems that already have permission models, and it should defer to them rather than replace them.

The short version for anyone implementing one this week: the trigger is allowed to identify who is asking and what they want. It is not allowed to be the thing that decides whether they may have it. Keep those two jobs in different systems and most of this article is already handled.

Give the bot a permission model people can see

A usability point that is also a security one. When a chat trigger refuses someone, the reply should say why in terms they can act on: you are not linked to a code-host account, or your account does not have write access to this repository.

Silent refusals and generic errors produce workarounds — someone asks a colleague to run it for them, which defeats the authorisation entirely while looking like helpfulness. A clear message routes the person to the actual fix, which is getting the access they should have or accepting that they should not.

A useful way to sanity-check any chat integration before shipping it: ask what the worst thing is that a person outside your organisation, sitting in a channel they were legitimately invited to, could cause to happen. If the answer is "nothing, they are not mapped", the design is sound. If it takes more than a sentence to work out, the authorisation is happening in too many places to reason about.

Takeaway

A chat channel is the loosest identity surface in most organisations — guests, externals, forgotten integrations — and a chat trigger connects it to your repository's permissions. Authorise against an explicit identity mapping checked at the code host, never against channel membership, and refuse unmapped users outright. Take the brief only from the invoking message rather than the thread, keep triggerable work bounded and reversible, have under-specified requests restate before acting, and write the provenance into the pull request because chat retention is not yours.

Keep reading
Codex vs Claude

What a Tool Protocol Standardises, and What It Leaves to You

Connection and discovery are solved; tool design is not. Context cost per attached server, why a CLI often beats a server, error messages as agent input, splitting read from write, and the small server over your own systems that nobody else can write.

Codex vs Claude

Image Generation Through MCP: A Credential, an Egress Path and a Spend Line

Attaching an image server to an agent adds four things at once. Constraining the write path, capping the spend, caching on a prompt hash, and why a shell script often beats a protocol.

Tooling & Integration

Building a Local MCP Server: Exposing What the Filesystem Cannot Answer

A working TypeScript MCP server over stdio, why the tool description decides whether it is ever called, returning errors the model can read, and the read-only role to create before it runs.

Tooling & Integration

Giving Claude a Second Opinion: Connecting Gemini Through a Local MCP Server

A working walkthrough for wiring another model into Claude Code as a tool: the server code, the registration command, the flag ordering that breaks it, and why the context parameter is the part people get wrong.

← The Price of Flagship Model Access  ·  Detailed Questions Up Front or Immediate Implementation? →

All codex vs claude articles  ·  Every article