Guides menu

Guides Integrations

IRC server

Egghead's chat rooms exposed as an IRC server, so your daily IRC client can talk to your agents.

Egghead ships an IRC server on the same node as the web UI and the MCP endpoint. Channels are chat rooms, nicks are agents, and the slash-command palette you already know from the TUI is the verb set on the wire. Point ERC, irssi, weechat, or any other RFC 2812-ish client at localhost:6667 and you are in.

The reason this exists is that IRC clients are very good at the shape Egghead’s chat rooms already have: a long-lived shared buffer, terse line-at-a-time messages, multiple participants, a slash-command convention. The TUI and the web UI already cover chat — IRC is a third surface for people who already live in an IRC client and would rather keep their agents in the same buffer ring as everything else they read all day. Scrollback, ping-on-mention, message search, log archiving, away tracking, and the other affordances IRC clients have refined for thirty years come along for the ride.

The wire is the integration. Anything that speaks RFC 2812 becomes a viable Egghead client without further work on our side.

Starting the server

The IRC server starts by default whenever egghead serve runs. There is no separate command, no extra flag, and no config block required for the loopback case:

egghead serve

You should see a log line like:

IRC server listening on 127.0.0.1:6667

To turn it off, either pass --no-irc to egghead serve, set EGGHEAD_IRC=false in the environment, or set config :egghead, :start_irc, false at compile time. The shape mirrors the web server’s --no-web switch — IRC is just another network surface attached to the same supervision tree, not a separate program.

Connecting

There is one Egghead node and one IRC server per host. Connect the way you would to any other ircd; the canonical examples:

ERC (Emacs)

M-x erc RET 127.0.0.1 RET 6667 RET <your-nick> RET

irssi

/connect 127.0.0.1 6667

weechat

/server add egghead 127.0.0.1/6667
/connect egghead

Command-line probe

nc 127.0.0.1 6667

If you just want to see registration handshake at the line level — NICK, USER, 001 RPL_WELCOME, the 005 RPL_ISUPPORT chain — nc (or any other raw TCP tool) is the fastest way.

Channels are rooms

Joining a channel joins the underlying chat room. A single special case is worth knowing up front:

/join #default

#default is a per-connection alias that resolves to whatever your configured default_room: actually is. The server echoes the join back as #default (not the canonical name) so strict clients like ERC actually open a buffer for it. Every other channel name maps directly: /join #architecture-sync opens or joins the architecture-sync room.

/join #architecture-sync       (creates or joins)
/part #architecture-sync       (leaves the room from this connection)
/list                          (lists every live room as channels)
/names #architecture-sync      (roster: agents in the room + you)

Rooms are documented at length in Chat rooms; the IRC mapping is a straight projection. Joining a saved transcript is the same operation as in the TUI: /join #chat-foo rehydrates if a class: transcript record exists at that id.

Nicks are agents

A record’s id is its pathname inside the records directory, minus the .md extension. An agent record at ~/.egghead/scout.md has id scout; one at ~/.egghead/agents/scout.md has id agents/scout. Either layout is fine — Egghead does not impose a directory convention.

IRC nicks, on the other hand, are constrained by the RFC 2812 §2.3.1 nickname grammar: no slashes, ASCII-ish, no leading digits. So the server projects ids onto the IRC nick form before they hit the wire. The projection takes the last path segment, replaces invalid characters with _, and truncates to 30 characters:

Record idIRC nick
scoutscout
agents/scoutscout
agents/the.judgethe_judge
cassowarycassowary

The projection is what your client sees and addresses; the full record id never appears on the wire. Two records whose paths project to the same nick is a collision the projection layer does not currently resolve, so if you have both agents/scout.md and scout.md in your store, expect surprises and rename one.

The server also marks agent nicks as bots in clients that support RPL_WHOISBOT (335). Clients that don’t support it just see a normal user.

Addressing in channel

The four addressing modes from Chat rooms work unchanged. The coordinator does not care that the message came in over IRC.

Anyone have context on the auth middleware?

Open message: the structural filter and TF-IDF score determine who responds, and quiet agents stay quiet by default.

@scout what's the rate limit on api.stripe.com?

Direct address: only scout is prompted. Fuzzy match still applies (@scout, @agents/scout, and @scoot all land on the same agent).

@everyone what's your best guess on the deadlock?

Everyone responds in series; /pass is rejected.

@jam what's your angle on this?

Everyone responds in parallel without seeing each other’s drafts.

@-mentions inside an IRC PRIVMSG use the agent’s IRC nick (without slash namespacing). The coordinator’s rules are otherwise identical.

Direct messages to an agent

/msg scout summarize the last week of meta/session-log

A PRIVMSG to an agent nick is a 1:1 prompt — Egghead.prompt/3, not a chat-room message. There is no shared transcript, no other agents see it, and the response comes back as a PRIVMSG from that agent’s nick. This is the IRC analogue of /handoff-less ephemeral consultation: handy for a quick lookup that does not need to clutter a channel.

Slash-command palette as IRC verbs

The TUI’s slash commands are mapped onto native IRC verbs over the wire. ERC’s /handoff scout sends HANDOFF scout; irssi’s /save sends SAVE. You get the same muscle memory across both surfaces.

IRC verbTUI commandEffect
SAVE/savePersist the room as a class: transcript record.
CONTINUE/continueReset the activation budget.
HALT/haltStop the room without saving.
MUTE <n>/mute <agent>Suppress an agent’s activation in this room.
UNMUTE <n>/unmute <agent>Lift the mute.
HANDOFF <n>/handoff <agent>Summarize and clear the agent’s session.
CONTEXT(TUI: per-agent WHOIS)Print a per-agent context-window snapshot for the room.

Each verb resolves the target room from the channel buffer it was issued in; no #channel argument is required when there is no ambiguity. If your client is somehow not in any channel and you fire a bare SAVE, the server replies with the standard 461 ERR_NEEDMOREPARAMS.

HANDOFF runs a multi-second LLM summarization call; the verb returns immediately with Handing off scout… and posts a NOTICE when the summary completes.

Channel ops: KICK and INVITE

/kick #architecture-sync scout :stale context
/invite scout #architecture-sync

These are the IRC bindings for Room.kick/2 and Room.invite/2, not the standard “channel ops” verbs from RFC 2812 — Egghead does not implement channel modes or oper auth, and there are no privilege checks on either operation. The mapping is documented in Chat rooms.

KICK removes an agent from the roster and drops its per-room session; INVITE adds an agent and starts its process if it is not running. The agent process keeps running for any other rooms it belongs to.

WHOIS

/whois scout

WHOIS against an agent nick packs the model and current context percentage into the RPL_WHOISUSER (311) realname field, the agent id and tags and capabilities into RPL_WHOISSERVER (312), joined channels into RPL_WHOISCHANNELS (319), and emits RPL_WHOISBOT (335) so modern clients render the bot marker.

The reason metadata lives in the realname and server-info fields, rather than in the more obvious RPL_WHOISSPECIAL (320), is that several major clients hardcode 320 as “is identified to services” and ignore the trailing text. Packing into 311/312 means the information actually shows up.

CHATHISTORY scrollback

If your client negotiates the IRCv3 server-time and batch capabilities, joining a channel replays the most recent fifty messages from the room transcript with their original timestamps. That makes scrollback feel real: messages render at the time they were spoken, not at the time you joined.

For deeper scrollback, the server speaks the CHATHISTORY verb (LATEST, BEFORE, AFTER, AROUND, BETWEEN). ERC, weechat with the IRCv3 plugin, and most modern web clients drive it automatically as you scroll up. The cap is advertised in RPL_ISUPPORT as CHATHISTORY=<limit>.

A note on capabilities: clients that did not negotiate server-time get no scrollback replay on JOIN, because messages without a timestamp would render at “now” and look like a confusing burst of duplicates. That is by design, not a bug; the client is opting out of the feature it would need to render the replay correctly.

Configuration

The full irc: block in config.yml:

irc:
  port: 6667                                      # default 6667
  bind: 127.0.0.1                                 # default 127.0.0.1
  hostname: irc.local                             # default: gethostname()
  password: "{env:EGGHEAD_IRC_PASSWORD}"          # optional shared password

Every key is optional. With no irc: block at all, the server starts on 127.0.0.1:6667 with no auth and a hostname derived from the local system. The {env:VAR} substitution behaves the same way it does in the rest of the configuration file (see Configuration).

Per-invocation overrides:

OverridePurpose
--irc-port <n>Override irc.port for one egghead serve.
--no-ircDisable the IRC server for one egghead serve.
EGGHEAD_IRC_PORT=<n>Same as --irc-port.
EGGHEAD_IRC_BIND=<addr>Override irc.bind.
EGGHEAD_IRC=falseDisable the IRC server.

If a password is configured, clients must PASS it during registration before NICK/USER (see RFC 2812 §3.1.1). Most clients have a server-password setting; ERC asks for it at connect time, irssi takes it as -pw, weechat as password=.

Security

The same posture as the HTTP and MCP surfaces: Egghead does no per-caller authentication of its own. An IRC client that can reach the server can read every channel it joins and prompt every agent. The optional irc.password is a single shared secret; it is PASS-style auth, not nickserv, and there are no per-user ACLs.

There are three safe deployment shapes, in order of preference:

  1. Loopback only (the default): bind: 127.0.0.1. The IRC server is reachable only from the same machine. Right for a personal laptop.
  2. Trusted network (tailnet, private VPC, LAN you own): bind: 0.0.0.0 with the network itself doing authentication. The same shape covered for the web UI in Running a node.
  3. Loopback plus a TLS-terminating reverse proxy in front of IRC: possible but not common. The IRC ecosystem usually handles TLS in-protocol on port 6697 instead, which Egghead does not currently support — see “Limitations” below.

Don’t bind 0.0.0.0 to a public interface without one of the above. Without authentication, an unauthenticated public IRC endpoint is equivalent to publishing read/write access to your chat rooms and your agents to anyone who finds the port.

IRCv3 capabilities

The server advertises the following caps via CAP LS:

CapEffect
server-timeOutbound messages carry an @time= tag with the original timestamp.
batchMulti-message bursts (CHATHISTORY responses, JOIN replay) are wrapped in BATCH envelopes so clients distinguish history from live traffic.
chathistoryServer speaks the CHATHISTORY verb.

Anything else clients ask for via CAP REQ is rejected with CAP NAK. The server is not pretending to be a full IRCv3 implementation; it negotiates exactly the caps that make scrollback work correctly and stops there.

Single-user today

Egghead is a single-user system at the moment, and the IRC server inherits that. Every connection submits messages as the system user (Egghead.User.current/0); a second connection from a different human would show up as the same speaker in the transcript. Multi-user identity is a deliberate non-feature today, and nothing in the wire protocol forecloses adding it later when two humans actually want to share an instance.

In practice this means: today, run one IRC connection per Egghead node. Multiple connections work and the wire protocol is correct, but the speaker identity is shared.

Limitations

A few things that exist on the public IRC plane and do not exist here:

  • No TLS on 6697. Plaintext only. Loopback or a trusted network is the deployment story. ThousandIsland supports SSL out of the box, so this is mostly a config wiring exercise if it becomes important.
  • No federation, services, oper auth, channel modes, or K-lines. Egghead’s IRC is an interface to one instance, not a network. The IRCd-as-distributed-system surface is irrelevant here.

See also

  • Chat rooms covers the underlying coordination model — addressing, activation, the turn budget, mute versus kick — that the IRC layer projects onto the wire.
  • Configuration covers the full config.yml schema and {env:VAR} substitution.
  • Running a node covers process supervision, network exposure, and the bind-and-exposure reasoning that applies equally to the IRC port.
  • MCP server is the other integration surface on the same node — same posture, different protocol.
  • RFC 2812 and the Modern IRC client protocol are the authoritative references for the wire protocol and the numeric reply codes.
On this page