Back in 1998, I was running an Eggdrop on EFnet and noticed something odd in my raw traffic logs. There were these messages wrapped in \x01 characters — binary delimiters sitting inside what I thought was a plain-text protocol. That was my introduction to CTCP. Twenty-five years later, CTCP is still there, embedded in every IRC client, quietly handling things most users never think about. Every time you type /me in a channel, you're sending CTCP. Every time your client responds to a VERSION request, that's CTCP too. Let me walk through exactly how it works at the wire level.
What CTCP Actually Is
CTCP stands for Client-To-Client Protocol. The name is precise. It's not a server protocol — the IRC server simply relays the messages without interpreting them. CTCP embeds structured requests inside regular PRIVMSG and NOTICE messages using a special delimiter: the ASCII character 0x01 (SOH, Start of Heading).
The original CTCP specification dates back to 1994, written by Klaus Zeuge and extended by various client authors. It was never formalized as an RFC. It exists as a de facto standard because every major client implements it. The specification has drifted over the years, with different clients implementing slightly different subsets.
The basic idea: wrap a command keyword and optional parameters between \x01 bytes, stuff it into a PRIVMSG, and the receiving client interprets it. Replies come back via NOTICE, also wrapped in \x01. The server sees it as normal message traffic. It has no idea what's inside.
# CTCP request (sent as PRIVMSG)
:sender!user@host PRIVMSG target :\x01COMMAND parameters\x01
# CTCP reply (sent as NOTICE)
:sender!user@host NOTICE target :\x01COMMAND response\x01
This distinction between PRIVMSG for requests and NOTICE for replies is fundamental. If a client sends CTCP replies as PRIVMSG instead of NOTICE, you get infinite reply loops between two clients. Some early clients had this bug. It was not fun.
Anatomy of a CTCP Message
Let's look at actual wire-level data. When you right-click someone's nick in HexChat and select "CTCP → Version", your client sends this:
PRIVMSG targetNick :\x01VERSION\x01
The target client's raw socket receives that exact string. The client parses it, finds the \x01 delimiters, extracts "VERSION", and generates a reply:
NOTICE yourNick :\x01VERSION HexChat 2.16.1 / Linux 6.1.0 [x86_64]\x01
Note the structure: the keyword (VERSION) comes first, then a space, then the response text. No colon separator after the keyword — that's a common implementation mistake.
Multiple CTCP commands can theoretically be embedded in a single message by placing them between separate \x01 pairs:
PRIVMSG nick :\x01VERSION\x01\x01PING 1234567\x01
In practice, very few clients support multiple CTCP commands per message. Most will only parse the first one. Don't rely on multi-CTCP in production.
There's also CTCP quoting for special characters. The original spec defines a quoting mechanism where \x01 inside CTCP data gets escaped. Modern clients mostly ignore this. If you need binary data inside CTCP, you're doing something wrong.
Standard CTCP Queries
Here are the CTCP commands you'll encounter in the wild:
VERSION — The most common CTCP query. Returns the client name and version string. Operators use this to detect botnets (50 identical VERSION replies = suspicious). Clients can fake this, so don't trust it for security decisions.
# Request
PRIVMSG nick :\x01VERSION\x01
# Typical replies
NOTICE sender :\x01VERSION irssi v1.4.4\x01
NOTICE sender :\x01VERSION WeeChat 4.1.2 (compiled Nov 15 2023)\x01
NOTICE sender :\x01VERSION mIRC v7.73 (Windows 11)\x01
PING — Measures round-trip latency between two clients. The sender includes a timestamp; the receiver echoes it back unchanged. The sender calculates the difference.
# Request (timestamp is seconds since epoch)
PRIVMSG nick :\x01PING 1701869423\x01
# Reply (echo back the same value)
NOTICE sender :\x01PING 1701869423\x01
Some clients use milliseconds. Some use a random integer. The spec doesn't mandate a format — the sender just needs to recognize its own value.
TIME — Returns the local time on the target client's machine. Mildly useful, mostly a novelty.
NOTICE sender :\x01TIME Thu Dec 07 14:23:45 2023\x01
ACTION — This is how /me works. When you type /me eats pizza, your client sends:
PRIVMSG #channel :\x01ACTION eats pizza\x01
The server relays it as a normal PRIVMSG. The receiving clients parse the \x01ACTION\x01 wrapper and display it as an action. This is probably the most-used CTCP command, and most users have no idea it's CTCP.
FINGER — Returns a user's "real name" or custom info string. Rarely used today. Some clients have removed it entirely.
USERINFO — Similar to FINGER. Returns a user-configurable info string. Also mostly dead.
CLIENTINFO — Returns a list of CTCP commands the client supports. This is the CTCP equivalent of asking "what can you do?"
NOTICE sender :\x01CLIENTINFO ACTION PING VERSION TIME DCC CLIENTINFO\x01
The DCC Handshake via CTCP
DCC (Direct Client-to-Client) connections use CTCP for the initial handshake. The actual data transfer happens over a direct TCP connection between the two clients, bypassing the IRC server entirely. The CTCP message only carries the connection parameters.
Here's how a DCC SEND works:
# Sender offers a file
PRIVMSG receiver :\x01DCC SEND filename ip_as_integer port filesize\x01
# Real example: sending photo.jpg from 192.168.1.100 on port 5000
# IP 192.168.1.100 = 3232235876 (packed 32-bit integer)
PRIVMSG nick :\x01DCC SEND photo.jpg 3232235876 5000 1048576\x01
The IP address is encoded as a 32-bit unsigned integer. To convert 192.168.1.100:
# Python conversion
import struct, socket
ip_int = struct.unpack('!I', socket.inet_aton('192.168.1.100'))[0]
# Result: 3232235876
# Reverse
ip_str = socket.inet_ntoa(struct.pack('!I', 3232235876))
# Result: '192.168.1.100'
The receiver connects to the sender's IP and port via TCP, and the file transfer begins outside IRC. The server never sees the file data.
DCC CHAT works similarly but establishes a direct text chat session:
PRIVMSG nick :\x01DCC CHAT chat 3232235876 5001\x01
The problem with DCC in the modern world: NAT. Most users are behind routers. The sender's IP as seen by the IRC server is their public IP, but the port they're listening on is on their private LAN. Without port forwarding, the receiver can't connect.
Passive DCC (also called reverse DCC) was invented to work around this. Instead of the sender listening, the receiver listens and the sender connects to them. The token-based handshake looks different:
# Sender proposes passive DCC
PRIVMSG nick :\x01DCC SEND photo.jpg 0 0 1048576 token123\x01
# Receiver accepts with their listening address
PRIVMSG sender :\x01DCC SEND photo.jpg 3232235876 6000 1048576 token123\x01
The zero IP and port signal that this is a passive request. The token links the two messages. Support for passive DCC varies wildly across clients.
Custom CTCP Commands
Since CTCP is just structured text inside a PRIVMSG, nothing stops you from defining your own commands. Bots and scripts do this regularly.
On a network I operated in the mid-2000s, we had a custom CTCP command for operator verification:
# Bot sends challenge
PRIVMSG nick :\x01OPERVERIFY challenge_token_abc123\x01
# Operator's client (with script) replies with HMAC
NOTICE bot :\x01OPERVERIFY hmac_sha256_response_here\x01
The bot sent a random challenge. The operator's client had a script that computed an HMAC-SHA256 using a shared secret and replied automatically. Quick and dirty mutual authentication without modifying the IRCd.
Custom CTCP is also used by some clients for feature negotiation. mIRC's DCC whiteboard feature uses custom CTCP. Various file-sharing scripts use custom commands for search and transfer coordination.
If you're building a bot that uses custom CTCP, follow two rules: use PRIVMSG for requests and NOTICE for replies (to prevent loops), and pick a keyword that won't collide with standard commands. Prefix it with your project name.
Security Implications
CTCP has several security concerns that operators should understand:
Information disclosure. VERSION replies reveal your client, version, and operating system. FINGER and USERINFO can leak personal details. TIME reveals your timezone. An attacker fingerprinting targets starts with CTCP VERSION. On our network, we saw targeted exploits delivered based on VERSION replies — if a user ran a vulnerable mIRC version, they got a crafted DCC SEND within minutes.
Flood amplification. A single CTCP VERSION sent to a channel triggers replies from every user in that channel. If the channel has 500 users, the server processes 500 NOTICE messages. This is why most IRCds and services implement CTCP flood protection — rate-limiting or outright blocking CTCP to channels above a certain size.
# UnrealIRCd anti-CTCP flood settings (unrealircd.conf)
set {
anti-flood {
target-flood {
channel-privmsg 10:5; # 10 msgs per 5 seconds
channel-notice 5:5;
channel-tagmsg 5:5;
}
}
};
IP disclosure via DCC. DCC handshakes contain the sender's IP address in the CTCP message. Even if the server cloaks hostnames, a DCC request reveals the real IP. Some networks strip DCC CTCP messages for this reason. Others replace the IP with the cloaked version.
Social engineering. Bots sometimes send CTCP messages that look like system notifications: "Your client needs updating, visit http://malicious.site". These appear differently from normal messages in most clients, making them seem more authoritative to naive users.
Recommended mitigations for network operators: rate-limit CTCP replies (most clients already do this), block CTCP to large channels except ACTION, strip or rewrite DCC IP addresses for cloaked users, and educate your user base about what CTCP replies actually reveal.
CTCP in the Modern IRC Era
IRCv3 hasn't killed CTCP, but it's made some of it redundant. The server-time extension provides accurate timestamps without CTCP TIME. The message-tags extension offers a cleaner way to attach metadata to messages. Some have proposed replacing CTCP ACTION with a native message type.
But CTCP persists because it works. It's in every client. It requires no server support. ACTION alone guarantees CTCP's survival — you can't remove /me from IRC without a revolt. VERSION remains useful for operators. PING is genuinely handy for measuring client-to-client latency.
DCC usage has plummeted. With NAT, firewalls, and ubiquitous web-based file sharing, very few people use DCC SEND anymore. DCC CHAT survived slightly longer because it provided an encrypted-ish private channel, but that use case is now covered by OTR plugins or just using Signal.
If you're writing a new IRC client or bot in 2024, you should implement: ACTION (mandatory — users expect /me), VERSION (respond with something reasonable), PING (trivial to implement, useful for diagnostics), and CLIENTINFO (good practice). Everything else is optional.
# Minimal CTCP handler in Python (raw socket level)
def handle_ctcp(sender, target, message):
if not message.startswith('\x01') or not message.endswith('\x01'):
return None
ctcp_content = message.strip('\x01')
parts = ctcp_content.split(' ', 1)
command = parts[0].upper()
args = parts[1] if len(parts) > 1 else ''
if command == 'VERSION':
return f'NOTICE {sender} :\x01VERSION MyBot 1.0 / Python\x01'
elif command == 'PING':
return f'NOTICE {sender} :\x01PING {args}\x01'
elif command == 'ACTION':
# Display as action, no reply needed
print(f"* {sender} {args}")
return None
elif command == 'CLIENTINFO':
return f'NOTICE {sender} :\x01CLIENTINFO ACTION PING VERSION CLIENTINFO\x01'
return None
FAQ
Can I disable CTCP replies in my client?
Yes. Most clients let you disable CTCP replies entirely or selectively. In WeeChat: /set irc.ctcp.version "" disables VERSION replies. In HexChat, go to Settings → Preferences → Advanced → CTCP Replies. In irssi, /set ctcp_version_reply to an empty string. Disabling VERSION is reasonable from a privacy standpoint. Disabling PING makes it harder for others to measure latency to you, which may annoy operators.
Why does /me work everywhere but other CTCP commands are inconsistent?
ACTION is the only CTCP command that every client must implement because it's a core part of the IRC user experience. Other CTCP commands are optional. Some modern clients have dropped support for obscure commands like FINGER, USERINFO, or SOURCE. ACTION has survived because users expect /me to work. The other commands persist mainly for operator tooling.
Is CTCP encrypted when using TLS?
CTCP messages travel inside normal PRIVMSG/NOTICE commands. If your connection to the server uses TLS, the CTCP content is encrypted in transit between you and the server. However, CTCP messages between two clients traverse the server — so the server (and any services or admin) can read them. DCC connections, once established, are direct TCP and can optionally use TLS (DCC SSEND), but support is rare.
Can CTCP be used for bot command-and-control?
Historically, yes. Early botnets used CTCP for covert command channels because some monitoring tools only inspected regular PRIVMSG content and ignored the \x01-wrapped payloads. Modern IRC monitoring tools parse CTCP. Network operators should log CTCP traffic alongside normal messages when investigating abuse.