My first mIRC script was a quit message randomizer. It picked a random line from a text file and used it as my quit message. I was 15 and thought this was the height of programming. Twenty-five years later, I've written scripts that managed botnets of Eggdrops, scripts that interfaced with SQL databases through DLL calls, scripts that implemented entire web servers using mIRC's socket API. The mIRC scripting language is strange, inconsistent, poorly documented, and more capable than anyone who hasn't used it would guess. This article is the reference I wish I'd had in 1998.
The Language Nobody Designed
mIRC's scripting language was not designed in any formal sense. It grew organically across dozens of versions, with Khaled Mardam-Bey adding features as users requested them. The result is a language with no specification, no grammar definition, no formal type system, and no coherent design philosophy. It has variables, but they're all strings. It has flow control, but the syntax differs between aliases and remote events. It has functions (called "identifiers"), but they use $ prefix notation and some are properties of objects that don't quite exist as objects.
The three main script components:
- Aliases — Stored in the Aliases editor. Define custom commands and identifiers.
- Remotes — Stored in the Remote editor. Event handlers triggered by IRC events.
- Popups — Right-click menu entries. Not covered here — they're just aliases with a menu UI.
Script files use .mrc extension by convention. They can be loaded via /load -rs script.mrc (remote) or /load -a script.mrc (aliases). Multiple script files can be loaded simultaneously and they share global scope — which is both useful and dangerous.
; Basic syntax rules
; - Comments start with semicolon
; - Commands start with / in the input line, no / in scripts
; - Variables: %var (local or global)
; - Local var: var %x = value (exists only in current alias/event)
; - Global var: set %x value (persists across sessions, stored in vars.ini)
; - Identifiers: $identifier or $identifier(parameters)
; - String concatenation: $+ (joins without space)
; - Line continuation: | (pipe) separates multiple commands on one line
;
; Example:
; alias greet {
; var %name = $1
; msg $active Hello %name $+ , welcome!
; }
Aliases: Custom Commands
Aliases are the simplest script construct. An alias defines either a command (executed with /aliasname) or an identifier (called with $aliasname). If the alias returns a value, it's an identifier. If it performs actions, it's a command. The same alias can be both.
; Command alias — join multiple channels at once
alias joinall {
join #channel1,#channel2,#channel3
}
; Identifier alias — returns a formatted timestamp
alias ts {
return $asctime(yyyy-MM-dd HH:nn:ss)
}
; Usage: echo -a Current time: $ts
; Output: Current time: 2023-11-12 14:23:05
; Alias with parameters — $1 is first param, $2 second, $1- is all params
alias say {
if (!$1) {
echo -a Usage: /say
return
}
msg $1 $2-
}
; Usage: /say #mychannel Hello everyone
; Multi-purpose alias: command and identifier
alias randcolor {
var %colors = 4 7 8 9 11 12 13
var %pick = $gettok(%colors, $rand(1, $numtok(%colors, 32)), 32)
if ($isid) {
; Called as $randcolor — return the value
return %pick
}
; Called as /randcolor — apply to text
echo -a $chr(3) $+ %pick $+ $1-
}
; Control flow — if/elseif/else, while, goto
alias countdown {
var %i = $iif($1 isnum, $1, 5)
while (%i > 0) {
msg $active %i $+ ...
dec %i
; NOTE: This fires instantly. To add delay, use /timer
}
msg $active Go!
}
; Better countdown with timers (non-blocking)
alias countdown2 {
var %total = $iif($1 isnum, $1, 5)
var %i = 1
while (%i <= %total) {
var %val = $calc(%total - %i + 1)
.timer 1 %i msg $active %val $+ ...
inc %i
}
.timer 1 $calc(%total + 1) msg $active Go!
}
The timer system is how mIRC handles asynchronous operations. There are no callbacks, no promises, no async/await. You schedule a command to run after N seconds with /timer. Named timers can be started, stopped, and queried. It's primitive but it works for the sequential, event-driven nature of IRC.
var for local variables in aliases. Global variables (created with set) persist to disk and survive mIRC restarts. Leaving temporary data in global variables is a memory leak that accumulates over weeks. I've seen mIRC instances with vars.ini files exceeding 10 MB because scripts set global variables in tight loops and never unset them.Remote Events: Reacting to Everything
Remote events are the core of mIRC scripting. They trigger on IRC events: messages, joins, parts, kicks, modes, CTCPs, raw numerics — anything the server sends. The syntax is on [ACCESS]:EVENT:MATCH:TARGET:{code}.
; Respond to a specific message in any channel
on *:TEXT:!help*:#:{
if ($2 == commands) {
notice $nick Available commands: !help, !time, !ping, !seen
}
elseif ($2 == $null) {
notice $nick Type !help commands for a list.
}
}
; Track when users were last seen
on *:TEXT:!seen *:#:{
var %target = $2
if ($hget(seen, %target)) {
var %data = $hget(seen, %target)
var %time = $gettok(%data, 1, 32)
var %chan = $gettok(%data, 2, 32)
var %msg = $gettok(%data, 3-, 32)
msg $chan %target was last seen $duration($calc($ctime - %time)) ago in %chan saying: %msg
}
else {
msg $chan I haven't seen %target.
}
}
; Record messages for !seen
on *:TEXT:*:#:{
hadd -m seen $nick $ctime $chan $1-
}
; Greet users on join (with flood protection)
on *:JOIN:#mychannel:{
; Don't greet ourselves
if ($nick == $me) return
; Don't greet if we greeted this user in the last hour
if ($hget(greeted, $address)) return
hadd -mu3600 greeted $address 1
.timer 1 2 msg $chan Welcome to $chan $+ , $nick $+ ! Type !help for info.
}
; Auto-op users by hostmask
on *:JOIN:#:{
if ($address($nick,2) iswm *!*@trusted.host.com) {
.timer 1 1 mode $chan +o $nick
}
}
; React to kicks
on *:KICK:#:{
; $knick = the kicked user, $nick = the kicker
if ($knick == $me) {
.timer 1 5 join $chan
echo -s I was kicked from $chan by $nick ( $+ $1- $+ ). Rejoining in 5 seconds.
}
}
; Raw events — handle any server numeric
raw 332:*:{
; 332 = topic reply
; $2 = channel, $3- = topic text
echo -a Topic for $2 $+ : $3-
haltdef ; prevent default handling
}
; CTCP handling
ctcp *:VERSION:?:{
ctcpreply $nick VERSION mIRC Custom Script v2.0
halt ; prevent default VERSION reply
}
The access levels system allows scripts to restrict who can trigger events. By default, * means any user. Access levels can be defined in the Users tab of the Script Editor:
; Users list (in Script Editor > Users tab)
; 100:marco!*@*.trusted.net
; 50:*!*@known-isp.com
; 1:*!*@*
;
; Then in remote events:
; on 100:TEXT:!admin*:#:{ ... } ← only level 100+ users trigger this
; on 50:TEXT:!mod*:#:{ ... } ← level 50+ users
; on 1:TEXT:!public*:#:{ ... } ← everyone
Hash Tables: In-Memory Data Storage
Hash tables were added in mIRC 5.8 and they changed what was possible with mIRC scripting. Before hash tables, persistent data meant reading and writing text files with $read() and /write — slow for large datasets. Hash tables provide O(1) key-value lookups in memory.
; Create a hash table with 100 buckets
hmake userdata 100
; Add entries
hadd userdata marco country:Italy joined:2023-01-15 level:admin
hadd userdata sarah country:UK joined:2023-03-20 level:mod
; Retrieve
echo -a Marco's data: $hget(userdata, marco)
; Output: country:Italy joined:2023-01-15 level:admin
; Parse a specific field
alias getfield {
; $getfield(table, key, field)
var %data = $hget($1, $2)
var %i = 1
while ($gettok(%data, %i, 32)) {
var %token = $gettok(%data, %i, 32)
if ($gettok(%token, 1, 58) == $3) {
return $gettok(%token, 2-, 58)
}
inc %i
}
return $null
}
; Usage: echo -a Marco is from $getfield(userdata, marco, country)
; Output: Marco is from Italy
; Save hash table to file
hsave userdata userdata.hsh
; Load hash table from file on startup
on *:START:{
hmake userdata 100
if ($isfile(userdata.hsh)) {
hload userdata userdata.hsh
echo -s Loaded userdata: $hget(userdata, 0).item items.
}
}
; Iterate over all entries
alias listusers {
var %i = 1
while ($hget(userdata, %i).item) {
var %key = $hget(userdata, %i).item
var %val = $hget(userdata, %key)
echo -a %key => %val
inc %i
}
}
; Hash tables with auto-unset (-u flag): entries expire after N seconds
; Great for cooldown timers
hadd -mu300 cooldowns marco 1 ; expires in 300 seconds
; Check if cooldown is active:
; if ($hget(cooldowns, $nick)) { notice $nick Please wait. }
hsave regularly and be prepared for data loss if mIRC crashes before saving. There is no transactional safety.Regular Expressions in mIRC Script
Regex support arrived in mIRC 5.9 via the $regex() identifier and /regex event matching. mIRC uses the PCRE library internally, so the regex flavor is Perl-compatible.
; Basic regex match — returns 1 if match, 0 if not
if ($regex(test123, /^[a-z]+\d+$/)) {
echo -a Matched!
}
; Capture groups — stored in $regml(N)
var %text = The temperature is 23.5°C today
if ($regex(%text, /(\d+\.?\d*)°([CF])/)) {
var %temp = $regml(1) ; 23.5
var %unit = $regml(2) ; C
echo -a Temperature: %temp degrees %unit
}
; Regex in remote events (on *:TEXT pattern)
on *:TEXT:*:#:{
; Detect URLs in channel messages
if ($regex($1-, /https?:\/\/([\w.-]+)/)) {
var %domain = $regml(1)
echo -a URL detected from $nick in $chan $+ : %domain
}
}
; Replace with regex — $regsubex(input, /pattern/, replacement)
alias striphtml {
return $regsubex($1-, /<[^>]+>/, )
}
; Usage: echo -a $striphtml(Hello world)
; Output: Hello world
; Regex replace with backreferences
alias censor {
; Replace bad words with asterisks of same length
return $regsubex($1-, /\b(badword1|badword2|badword3)\b/gi, $str(*, $len(\1)))
}
; Named capture groups (PCRE)
var %line = [2023-11-12 14:30:05] Hello
if ($regex(%line, /\[(?P[^\]]+)\]\s<(?P[^>]+)>\s(?P.*)/)) {
echo -a Date: $regml(date) Nick: $regml(nick) Message: $regml(msg)
}
Sockets: TCP Connections From Script
mIRC's socket API lets scripts open TCP connections to arbitrary hosts. This enables HTTP requests, custom protocol implementations, and inter-bot communication. The API is event-driven: you open a connection, and mIRC fires events for connect, read, close, and error.
; Simple HTTP GET request using mIRC sockets
alias httpget {
; Usage: /httpget example.com /api/data
var %host = $1
var %path = $iif($2, $2, /)
sockclose httpget
sockopen httpget %host 80
sockmark httpget %host %path
}
on *:SOCKOPEN:httpget:{
var %host = $gettok($sock(httpget).mark, 1, 32)
var %path = $gettok($sock(httpget).mark, 2-, 32)
sockwrite -n httpget GET %path HTTP/1.1
sockwrite -n httpget Host: %host
sockwrite -n httpget Connection: close
sockwrite -n httpget User-Agent: mIRC-Script/1.0
sockwrite -n httpget
}
on *:SOCKREAD:httpget:{
var %line
sockread %line
; Skip HTTP headers
if (%line == $null && !%httpget_body) {
set %httpget_body 1
return
}
if (%httpget_body) {
echo -a [HTTP] %line
}
}
on *:SOCKCLOSE:httpget:{
unset %httpget_body
echo -a [HTTP] Connection closed.
}
; TLS socket (HTTPS) — requires mIRC 7.0+
alias httpsget {
var %host = $1
var %path = $iif($2, $2, /)
sockclose httpsget
sockopen -e httpsget %host 443
sockmark httpsget %host %path
}
; Socket server — listen for incoming connections
alias startserver {
socklisten myserver 8888
echo -a Listening on port 8888
}
on *:SOCKLISTEN:myserver:{
var %name = client. $+ $ticks
sockaccept %name
echo -a Accepted connection: %name
}
on *:SOCKREAD:client.*:{
var %data
sockread %data
echo -a [%data] from $sockname
sockwrite -n $sockname Echo: %data
}
The socket API is how people built web-based IRC statistics pages, API integrations, and inter-process communication in the mIRC era. It's clunky by modern standards — no async/await, no promises, just event handlers and global state — but it works for single-threaded, event-driven IRC workflows.
Dialogs and DLL Calls
Dialogs give mIRC scripts a graphical interface. You define dialog layouts in script, then display them. The dialog definition language is declarative — you specify controls (buttons, text fields, list boxes, check boxes) with positions and sizes.
; Dialog definition
dialog userinfo {
title "User Information"
size -1 -1 200 150
option dbu
text "Nickname:", 1, 10 10 50 10
edit "", 2, 65 10 120 10
text "Channel:", 3, 10 25 50 10
edit "", 4, 65 25 120 10
text "Level:", 5, 10 40 50 10
combo 6, 65 40 120 60, drop
check "Auto-op on join", 7, 10 60 100 10
check "Auto-voice on join", 8, 10 75 100 10
button "Save", 9, 40 120 50 14, ok
button "Cancel", 10, 110 120 50 14, cancel
}
; Open the dialog
alias edituser {
dialog -m userinfo userinfo
; Populate combo box
did -a userinfo 6 User
did -a userinfo 6 Moderator
did -a userinfo 6 Admin
}
; Handle dialog events
on *:DIALOG:userinfo:init:*:{
; Set defaults when dialog opens
did -a userinfo 2 $me
did -a userinfo 4 #mychannel
did -c userinfo 6 1 ; Select first combo item
}
on *:DIALOG:userinfo:sclick:9:{
; Save button clicked
var %nick = $did(userinfo, 2)
var %chan = $did(userinfo, 4)
var %level = $did(userinfo, 6)
var %autoop = $did(userinfo, 7).state
var %autovoice = $did(userinfo, 8).state
echo -a Saved: %nick in %chan as %level (op: %autoop voice: %autovoice)
}
DLL calls extend mIRC beyond what the scripting language alone can do. mIRC can load Windows DLLs and call exported functions. This was the gateway to database access, cryptography, system APIs, and anything else that C code could do.
; DLL call syntax
; $dll(filename.dll, function, parameters)
; The DLL function must conform to mIRC's calling convention:
; int __stdcall function(HWND mWnd, HWND aWnd, char *data, char *parms, BOOL show, BOOL nopause)
; Example: calling a hypothetical database DLL
alias dbquery {
var %result = $dll(mircdb.dll, query, SELECT nickname FROM users WHERE level > 50)
echo -a Query result: %result
}
; Example: calling a hashing DLL
alias md5 {
return $dll(mircrypto.dll, md5, $1-)
}
; Usage: echo -a $md5(Hello World)
; Note: mIRC 7.x added built-in $sha1, $sha256, $md5 identifiers
; so DLLs are less needed for common operations now
The DLL interface made mIRC extensible in ways Khaled probably never anticipated. People wrote DLLs for SQLite access, for HTTP client functionality (before mIRC had built-in sockets), for MP3 playing, for capturing screenshots, and for interfacing with Windows COM objects. The mIRC scripting community was, in a very real sense, building a plugin ecosystem through the DLL interface years before browser extensions or VS Code extensions existed.
The language has its limits. No classes. No modules. No namespaces beyond script file separation. No proper error handling — a bad variable reference doesn't throw an exception, it silently returns empty string. No debugging tools beyond echo statements. No package manager. No dependency resolution. And yet, people built remarkable things with it. The constraint bred creativity. When your only tool is a strange, typeless scripting language embedded in an IRC client, you learn to make it do things it was never intended to do. That's the story of mIRC scripting: a language nobody designed, used by thousands to build things nobody expected.
FAQ
Can I still write mIRC scripts in 2023?
Yes. mIRC is still updated and the scripting engine is fully functional in current versions. The language hasn't changed significantly since the early 2010s, so older tutorials and references still apply. The community is smaller, but #mIRC on EFnet and various scripting forums are still active.
What's the maximum size of a mIRC script?
There's no hard limit on script file size, but mIRC's script editor becomes sluggish with files larger than about 50,000 lines. For large projects, split code across multiple .mrc files and load them all. Global variables and hash tables are shared across loaded scripts.
How do I debug mIRC scripts?
Use echo -a to print variables and state to the active window. Use echo -s to print to the Status window. Use /debug -o filename.log to log all IRC traffic to a file for protocol-level debugging. There is no step debugger, no breakpoints, and no stack traces. Welcome to 1996.
Are mIRC scripts a security risk?
Yes. Scripts can execute system commands via $os, /run, and DLL calls. Malicious scripts disguised as useful tools circulated widely in the 2000s — war scripts with embedded backdoors, supposed protection scripts that actually gave remote access to the author. Never load a script from an untrusted source without reading every line. If it calls /run, $dll, or uses socket connections to unusual hosts, treat it as suspicious.