About the Game
You're a hacker with a terminal, four terrible scripts, and an anonymous contact who needs files from places they really shouldn't be. The scripts were free, and you can tell.
Your port scanner checks all 65,535 ports, which is thorough and also going to take longer than you have patience for. Your password cracker just brute-forces everything and wakes up the intrusion detection system doing it. Both will technically get the job done, the way a spoon will technically dig a tunnel.
So you rewrite them, and the new probe only checks common ports and finishes in seconds, while the new cracker skips guessing entirely and reads the admin's own password hints straight off the filesystem. Nobody told you to fix these - you just got tired of using tools this bad.
That's Blackdoor - you write the code, the code hacks the servers, and better code hacks them faster, quieter, and before the trace catches up.
Not a hacking-themed clicker. An actual terminal.
There are no buttons, no "press X to hack", no minigames where you rotate pipes or match symbols. You sit at a command line with tab completion, a built-in code editor, and the full set of commands you'd expect - and you write scripts in a custom Python-like language that was built to be fun to learn and satisfying to master. Every script executes line by line, so you see exactly where it works, where it breaks, and how much faster it gets once you're the one who rewrote it.
If you've never programmed before, it's more sink-or-swim than a tutorial - you're straight into scripts that already work but could work better, picking up variables, loops, and functions by actually touching them. If you have, you'll skip past the basics quickly and start writing proper tooling. The language is close enough to Python that your instincts will carry over. Either way, the regulars will accidentally teach you things by arguing about it in IRC.
The Job
Your contact goes by second_hand, who operates through #blackdoor - a small IRC channel populated by hackers, script kiddies, and at least one person who thinks printing "hello" counts as a proof of concept - and doesn't do small talk. You get a server IP, a brief explanation of what's on it, "good luck", and payment on delivery.
The work escalates - unlocked test servers turn into corporate networks with intrusion detection, single-user boxes turn into multi-user environments where you crack a weak account and escalate privileges from inside, and simple downloads end up as encrypted files behind filtered ports you have to fight through first.
Every job harder than the last, solvable more than one way, with a story underneath it all about why your contact needs these files so badly - and what they plan to do with them.
Your code, your rules
There's no right answer - you get targets and tools, and how you combine them is entirely up to you.
Write a surgical probe that scans five ports and quits, or something that downloads every file on the server and sorts through it later, or just find the admin's password sitting in a text file some intern left in their home directory and skip cracking entirely - all of it works. The scripting language is simple enough to learn in minutes, deep enough that you'll still be optimising scripts hours later.
Spend your earnings on hardware upgrades (faster processing, more memory, better network) or on software tools that unlock new capabilities, or just write better code and keep the money - either way works.
SSH Isn't the Only Door
Not every server plays by the same rules, and not every lock needs the same key.
Some run web forms that hand over their whole user table to a stray quote mark, or a "diagnostics" page that quietly shells out to the OS underneath. A real browser automation API means you can script your way through a signup form as easily as an SSH session, watching it fill in and submit itself if you're looking at the right pane. Some gate the good stuff behind a file that's normally locked, only readable for a few seconds out of every cycle - keep trying, and grab it the moment it opens.
Some boxes run mail - connect on port 25, log into a mailbox, and read what people actually say to each other when they think nobody official is reading, or send a very convincing email as someone you're not and see who takes the bait.
All of it shows up as ordinary side work, posted by contacts in IRC same as everything else - just more ways for a job to go sideways.
You're not alone in here
You're never alone in #blackdoor. Hackers chat while you work - sharing tips, posting side jobs, placing bets on whether anon42 can find a port (he can't). Your contact DMs you mission briefings, and the channel reacts when jobs get done. Like any real IRC channel, most of the advice is unsolicited and some of it is wrong.
#lounge is a live channel where real players talk about anything - help, questions, scripts, general chat. It's not moderated in real time, though Steam's text filter still applies.
Share everything
Every script you write is a real file, and every server is a JSON config - edit scripts in the built-in editor, share them (or whole campaigns) through Steam Workshop, and download other players' work the same way. Anything you subscribe to shows up ready to use on your in-game drop server. No menus - just files on a server, like everything else.
Key Features
Write real code - A custom scripting language designed for hacking, not buttons or minigames. Your code runs line by line, and you see exactly why it's slow.
Hack servers - Probe ports, crack passwords, explore filesystems, escalate privileges, steal data. Every server has its own users, files, and weak points to find.
Race the trace - Every connection starts a countdown that speeds up the more noise you make, and getting traced ends it right there.
A real terminal - Tab completion, command history, a code editor - no theming, no skin over a menu, this is the actual interface.
Upgrade your rig - Faster hardware means faster scripts, better tools unlock new capabilities, or just save your money and write better code instead.
Story campaign - An anonymous contact, escalating jobs, a corporate conspiracy, and a secret that might be worth more than the pay.
IRC - Hackers chat, argue, and accidentally teach you things while you work, and side jobs and tips both get shared along the way.
Your approach - No fixed solution - write surgical tools or blunt instruments, find passwords in config files or brute-force them, and if you find a shortcut nobody else did, that's the point.
Live community chat - #lounge connects you to other players. Ask questions, share scripts, compare notes.
Workshop - Share scripts and entire campaigns, download community tools, and see how other people solved the same problems.
Screenshots
8 imagesVersion Information
Steam Patch Notes
Official update history
Script Panels
Scripts can claim their own real display panel now, instead of only ever printing lines and reading one at a time - open_panel() grabs one from a script panel in your layout.cfg. The Layout Designer can build one visually and export the file.
open_panel("log") claims a plain scrolling text panel instead of a character grid - just for a script's own log() output, so it can run with "visuals" without touching a single term_* function.
Once you've got a panel, term_write()/term_color()/term_move()/term_clear()/term_bg()/term_size() draw to a real character grid - your own colors, your own layout, cell by cell.
term_box()/term_hline()/term_vline() draw borders and lines for you, and term_fill() colors a rectangle instead of the whole panel.
term_click_zone() declares a clickable region without drawing anything itself - get_click() now returns which declared zone (if any) a click landed in.
term_scale() draws bigger text for a title or header, term_clear_rect() blanks just part of a panel, and term_cursor() hides the cursor block for a purely graphical display.
get_click()/get_key() poll for live mouse and keyboard input from inside your own loop - a script can react to a click or a keypress the moment it happens, not just read a line of typed text after the fact.
get_event() polls clicks and keys off one shared, ordered queue instead of two separate ones - useful the moment a panel has more than one clickable field, so a click that changes which field has focus and the keystrokes typed right after it always come back in the order they actually happened. get_click()/get_key() still work fine for a panel that only cares about one or the other.
Drawing and polling run at their own fast, fixed pace, completely separate from your CPU tier - none of it counts against the tick economy real hacking work costs against.
A script's reactive logic - deciding what to draw in response to a click or keypress, not just the drawing itself - can now live inside a render: block and run at that same free, fixed pace.
The terminal can be tabbed alongside other panes in layout.cfg now too, instead of always needing a slot to itself.
There are a couple of working examples of what all this can do already sitting out there in-game, if you go looking for them. A barebones one - just enough to see the shape of it - is up on the API reference too, alongside the rest of the docs at blackdoor.tools/docs.
CPU & Performance
A community bug report did real controlled testing across a dozen-plus isolated scripts to chase down a "CPU feels inconsistent" symptom to its actual root causes - genuinely excellent work, and worth a section of its own rather than getting buried in the usual bug-fix list.
The overload kill is supposed to fire after 5 seconds sustained at 100%+, but could instead fire anywhere from well under a minute to nearly twice that for the exact same maxed-out workload, run to run. The timer was ticking on your screen's own render rate instead of a fixed clock - a slower machine, or a minimized/unfocused window, could get killed well before it had actually earned it. It's on a fixed clock now, so the same workload behaves the same way every time.
get_cpu() could keep reporting a maxed-out reading for a while after your processes had actually already stopped, and gave inconsistent numbers if you polled it several times in a row - it was reading a smoothed display value instead of your real, live usage. It now always matches what's actually running right now.
Logging a concatenated string - log("progress: " + str(x)) and the like - quietly cost an extra CPU tick over logging a plain value. Fixed - concatenating inside a log() call is free again, same as everywhere else in the language.
get_unix_time()'s docs said it returns an int; it's always actually returned a float with real sub-second precision. Docs now match reality.
Bug Fixes
A @param default written in quotes (default="5") kept the quote characters as part of the string. Writing it without quotes (default=5) dodged that but got silently treated as a number even where a string was expected. Quoting a default now works the same way it does everywhere else in the language.
Cracking an account on an IDS-enabled server could rotate that account's own password the instant you finished cracking it - before you'd done anything with the access. Fixed - repeated failed login attempts still raise suspicion, cracking one on its own no longer does.
mount's /mnt sync only ever worked one way - a real file changed on disk would show up in-game, but editing, copying, moving, renaming, deleting, or creating a /mnt file in-game never touched the real file on disk at all. Fixed - all of that now writes straight through to the real file, same as an external editor's own save already did.
import "name" never looked inside /mnt at all, even though run could - a module sitting right next to the script importing it could fail to import just because it lived in /mnt. Fixed.
Crack Source's delivered script and docs read like it should match crack()'s speed - it can't, since interpreted .bd code costs real ticks a native function doesn't, and it would CPU-kill before finding a single character. The delivered script now disconnects right after grabbing the hash instead of at the very end, and the wording now leads with the actual point: nothing past that first hash grab needs the connection, so you can search offline instead of holding a live session open.
The daily FTP-focused and database-focused CTF boxes had no way to discover the username needed to get past the first flag - neither one has a root account, and nothing on either box ever revealed the real one. Fixed - it's now findable the same way the first flag already is.
An open FTP or database session could occasionally get treated as if it wasn't there - a typed command misfiring, or the connection appearing to drop - if something unrelated happened in the background at the same moment (a script finishing, erroring, or getting killed elsewhere). Fixed.
remote_run() (Pivot Source) and pivot() (ProxyChain) share "pivot" branding but do genuinely different things - remote_run() is a reachability tool only, it never masked your IP from a trace despite reading like it might. Its docs (in-game and on the site) now say so plainly instead of leaving it to infer.
Reputation could get ground all the way down to 0 by repeated HUNTED penalties, and nearly every mission past the opening tutorial needs at least 1 - a genuine dead end with nothing left to accept. Fixed - a "Quick Favour" job is now offered the moment you're below your best-ever reputation, so there's always a way back up even from 0.
Workshop content could still reset its visibility to public after republishing in some cases, even with the earlier fix for this in place. Fixed - your chosen visibility now survives every republish path.
Subscribed Workshop content could keep loading an outdated version after the creator pushed an update, instead of the current one. Fixed - it now checks for and grabs the latest version before loading, and the Workshop import list updates live the moment a subscription changes instead of needing a restart to notice.
If you'd ever edited a file on a Workshop server yourself (e.g. live-debugging a script through SSH), that edit could keep silently overriding the creator's own updates forever after - a republish with new content, or newly-enabled encryption, at that same file just wouldn't take. Deleted files could also reappear the same way. Fixed - your own edits still persist as before, but only for files the creator hasn't touched since; their own updates now always win where they've actually changed something.
Also New
cp supports a -r flag now, matching rm - copy a whole directory at once instead of one file at a time.
A bare run <name> now finds a script anywhere under /mnt, not just sitting directly at its top level - run foo finds /mnt/lib/foo.bd the same as it already found /mnt/foo.bd.
Where a bare script name gets looked up is configurable now - blackdoor.conf's new path= line takes a comma-separated list of directories (defaults to ~/scripts,/mnt if you don't set one). which <name> shows exactly where a name currently resolves to.
run script.bd > file (or >> file to append) sends everything a script logs to a file instead of the terminal - works in both foreground and background, and captures a real script error the same way.
Commands can be chained on one line now - cmd1 ; cmd2 runs each one in sequence, cmd1 && cmd2 only runs the second if the first succeeded, and cmd1 || cmd2 only runs it if the first failed.
remote_running(conn, script_path) (Pivot Source) checks whether a remote_run() is still going on the target, so a script can wait for one to finish before starting the next.
The files pane has a "run" action now for any .bd script, right alongside edit/read/delete - no need to type run <name> by hand just to try something you're already looking at.
netcat() challenge ports aren't limited to the built-in string-reversal puzzle anymore - a "challenge" port can now set challenge_type to math (a random arithmetic problem), fixed (your own author-written question/answer pairs), or script (a real .bd file on the server that computes its own question and answer, with the full language available to it). A new challenge_begin event also fires the moment someone connects, before the challenge itself is even generated.
The Workshop editor's port SERVICE field now shows which exact (case-sensitive) strings actually unlock real behavior - ssh, ftp, mysql/postgres/postgresql, http/https/http-alt, smtp, challenge. Anything else is fine for pure flavor, but a typo in one of these used to silently degrade a port to flavor-only with no error at all.
If your reputation dips below where it's been before, hexdump now offers a "Quick Favour" job to help close the gap - modest pay, but real reputation, until you're caught back up.
If a "script"-type challenge port's own .bd script has a bug, you'll now see the real error message if you're the Workshop author of that box (prefixed onto the same solvable fallback puzzle everyone else still gets) - so you can actually find and fix it instead of just wondering why it's using the fallback.
- Static
Bug Fixes
sqli() no longer crashes a script when a target has no configured routes - returns a normal checkable result instead.
Object literals, lists, and function calls can now be written across multiple lines.
ls now shows real file permissions and ownership, matching what download()/upload() actually enforce.
The web API reference's connect() entry now carries the same success-vs-login warning the in-editor reference already had.
The in-editor scripting reference now documents what a module's top level can and can't contain.
prompt() no longer discards whitespace-only input.
scp/upload() now correctly lands files inside a destination folder in every case.
Settings in a blackdoor.conf you find on another box now apply fully, same as your own.
theme= in blackdoor.conf now matches a preset regardless of capitalization.
Thanks for all the reports - this patch is basically nothing but them.
- Static
New
Action link syntax (open a firewall port, view server users, etc. from an HTML page) is now documented right in the Workshop editor when editing an .html file, instead of only existing in an external doc.
A new editor= option in ~/.config/blackdoor.conf (simple or nano) - nano mode adds a real Ctrl+X: prompts "Save modified buffer?" if you've got unsaved changes (Y or N then Enter to confirm, Esc cancels), or exits straight away if there's nothing to save. Defaults to simple, today's Ctrl+S/Esc behavior.
Ctrl+D now disconnects from a server, same as typing disconnect.
Ctrl+R searches command history, same as a real terminal - press it again for older matches, Enter to load one into the line, Esc to cancel.
Bug Fixes
"Import from Workshop" always said there was nothing to import, even for content you'd genuinely published yourself. Fixed - anything already published self-heals automatically the next time you check.
A new Workshop mission defaulted its "posted by" attribution to a random story NPC or "anon" instead of you, and the field to change it was hidden unless SIDE MISSION was toggled on. Now defaults to your own handle and is always visible and editable.
cmd_inject_vulnerable routes never actually triggered from a real form submission, only when a script called cmd_inject() directly.
The procedural Full Audit/daily/weekly contract generator's corporate "reply" job hinted at the wrong form to inject. Now has a real checkpoint objective and a corrected hint pointing at the right one.
Ctrl+Enter in the code editor could only quick-run a script sitting directly in ~/scripts/, not in a subfolder.
scan() used to halt a script entirely on an unreachable host - it now returns a normal, checkable result with reachable: false instead, matching how connect() already handles the same case.
The Workshop server editor's \[+ add user] button never actually added the new user to /etc/passwd or /etc/shadow.
A few "DM the answer" mission objectives were too strict about exact wording. These now accept more varied phrasing of the same correct answer.
save()/write_local(), write_file(), touch(), and touch_local() silently did nothing when the parent directory didn't exist. The write functions now return {success, path, error}; the touch functions return false.
local_mv()/local_cp() had the same silent-failure gap - now they auto-create missing directories instead, matching the terminal's own mv/cp and upload(). local_mv() on a directory now moves its contents with it too.
Workshop-authored "reply"/mail_sent objectives could be unreachable or fail to complete under some authoring conditions. Fixed.
- Static
New
The mission editor's IRC Chatter sequences can be edited after adding them now, not just deleted and retyped from scratch.
The mission editor's SERVER field is a real picker now, pulled from your own saved servers, instead of a free-text box.
Publishing a mission now bundles any server it references that isn't already its own separate Workshop item directly into the mission itself, instead of publishing it separately - no more one-off boxes cluttering the Workshop server list when they were only ever meant for one mission. A server you've deliberately published on its own gets linked as a required item instead, so subscribing to the mission pulls it in automatically.
Campaigns can be published to Steam Workshop now - previously the only option was a local export package, and subscribed campaigns were never actually loaded. Missions and servers a campaign references get the same bundle-or-link treatment.
Added a "publish as new" option, for when the Workshop item behind something you're republishing has been deleted directly from Steam.
Storage Drive++ is back in the hardware shop (400 file capacity).
music off/music on now actually mute and unmute, instead of just resuming playlist rotation - use music rotate for that now.
#lounge timestamps show the date alongside the time for messages that weren't sent today.
!$/!^ recall the previous command's last/first argument, bash-style.
/ignore <handle> and /unignore <handle> in the IRC input hide a player's #lounge messages for you, locally.
Purchased shop scripts now get a pristine backup copy on the drop box, under /tools/shop_scripts/ - same recoverable-backup idea as the starter scripts already there, in case you edit or delete your working ~/scripts/ copy.
Bug Fixes
Publishing to Steam Workshop from the main menu (before starting or loading a game) could hang forever on "Publishing to Workshop..." - Steam's async responses were never being processed there, so the item got reserved server-side but never finished uploading, leaving a permanent, empty listing behind.
A mission whose SERVER field was left holding an unreplaced template placeholder would silently vanish for every subscriber, with no error shown to the author or anyone else.
connect()/smtp_connect() could carry over authentication from a completely unrelated earlier connection - cracking a user on one server, then connecting to a different one without disconnecting first, could leave you looking authenticated somewhere you never actually cracked anything.
Two mission login pages (DataVault's and SecureStore's admin panels) were missing the form tag their scripted login relied on, so submit_form() silently failed on both - manual browser login was unaffected.
scrub_log() was writing an unrelated, self-incriminating log entry (your exact IP and how many lines you removed) into /var/log/syslog, regardless of what file you actually scrubbed. It still leaves something behind on purpose, but now in the file you actually cleaned, and without handing over your IP.
upload()'s local file path didn't accept relative paths, including ../ - only absolute paths and ~/ worked. It now resolves against your script's working directory, same as save(), local_mv(), and every other local-path function.
- Static
New
String formatting: "%s connected on port %d" % \["root", 22]. %s for any value, %d/%f for numbers, %% for a literal percent.
rm_dir(conn, path) - recursively delete a directory and everything inside it.
get_unix_time() - real wall-clock time, for benchmarking your own script's actual elapsed time.
Ctrl+/ in the code editor now comments/uncomments the selected lines (or just the current line).
A small "new output" indicator now appears if you're scrolled back in the terminal and something new prints.
Republishing an updated Workshop item now asks what actually changed, instead of always using a fixed generic note.
Piping is real now: cat file.txt | grep 'bob' works, and find ../ -name *.log | cat $_ runs cat once per result.
cd(conn?, path) and get_cwd(conn?) - scripts have a real working directory now.
Completed missions now show up in a collapsed list in the jobs panel. Retry any of them for a clean replay - no repeat payout.
Some performance improvements under the hood.
Updated Workshop editor tools - rename files and folders, a real multi-line editor for file content, and a few more authorable server fields.
Bug Fixes
Reconnecting to a server you were already tracing no longer resets the trace back to full.
upload() no longer silently writes files to a destination that doesn't exist yet.
mv and cp had the same issue as upload() above - fixed.
delete_file() on a directory now rejects it with a clear error instead of orphaning its contents. Use the new rm_dir() for that.
Added the missing "Skip Boot Sequence" toggle to the in-game settings menu.
pivot()'s "trace already active" error is clearer about why.
Mission tab text can now be selected and copied, and the target IP there is right-clickable too, same as the network panel.
help filesystem was missing several real commands - added.
#lounge could quietly stop receiving messages - it now reconnects and catches you up automatically.
Finishing an objective, saving, and reloading could reset it back to incomplete. Fixed.
Leaderboard/audit submissions could show your Steam profile name instead of your in-game handle. Fixed.
Progression/streak/skill badges weren't being awarded automatically. Fixed.
Shop page text and the bottom status bar didn't scale with the font size setting. Fixed.
Various mission hint/objective adjustments across the campaign.
ProxyChain's own shop page taught a pivot() workflow that never actually worked. Rewrote it to show the workflow that does.
A paused trace could regenerate fully and still stay flagged as active forever, permanently blocking pivot(). Fixed.
Fixed a login-gated dashboard page being reachable by just guessing its filename.
Cleaner parser errors.
Daily/weekly job briefings could describe a different objective than the one actually assigned. Fixed.
A first ever Workshop publish could silently create an empty, hidden item and orphan it on every retry. Fixed - a retry now correctly finishes the same reserved item.
- Static
New
Backup copies of the 5 starter scripts now live on the drop box, under /tools/starter_scripts/. Broke your own copy? Grab a fresh one any time.
publish <script> new - fixes scripts stuck retrying against a botched first publish attempt.
to_code(char) - inverse of to_char(code).
Output redirection: find ... > file / >> file now works for read/query commands (ls, cat, grep, find, history, etc).
vim/vi now alias to edit.
Script execution limit raised 10x (400,000 -> 4,000,000 ticks) for long, legitimate loops.
New setting: "Skip Boot Sequence" - quick fade instead of the full boot, both menu and in-game.
The network pane's stored hosts list can now be collapsed - click the \[+]/\[-] next to "STORED HOSTS" to hide/show it. Open by default. Your active mission's server(s) and your script's current \ target now show in their own always-visible groups above the collapsible list.
Accepting a mission now switches IRC to your contact's DM automatically, instead of leaving the briefing to arrive silently in whatever tab you already had open.
clear() - new scripting builtin, clears the terminal the same way the clear/cls command does.
round(n), floor(n), ceil(n) - new scripting math builtins.
Bug Fixes
Fixed "Demo Reel"'s impossible-as-briefed first objective.
Fixed "Leverage" being beatable by just cracking root, skipping the puzzle.
"Chain of Custody" and "Unravelling" now accept other legit routes to their files, not just the one intended path.
Fixed several achievement bugs: wrong shop-completion key, Workshop publish/subscribe never firing, cat-only file-read triggers, one-time Steam sync failures never retrying, and "Maxed Out" being unearnable (unreachable shop items removed).
Fixed missing/unalphabetized functions in the in-editor API reference panel.
Fixed "Diagnostics"'s hint showing an outdated function signature.
Fixed web ports not re-opening after a high-alert trace event.
Fixed forkbomb not pausing your trace, plus reconnects and danger visuals not respecting a paused one.
Fixed generated audit/daily/weekly contracts auto-completing on accept, and not reaching the leaderboard.
Fixed a custom blackdoor.conf theme not applying until re-saved.
Fixed "Six Clicks" being completable with a blank form submission.
Clarified "Paws Protocol"'s director memo hint.
Fixed submit_form() not telling scripts when a submission leaked SQL injection data.
Fixed tab-completion never working for publish <script>.
Fixed "Player not found" when clicking a name in IRC.
Fixed "Rug Pull"'s press-kit objective being uncompletable.
Fixed the weekly leaderboard showing empty due to wrong week-key math.
Fixed boards page table misalignment; leaderboard names are now clickable, same as IRC.
Fixed grep/grep -r on a remote server being able to read files you don't have access to (cat already checked this, grep didn't).
Fixed clicking \ in the files pane bypassing remote file permissions and "\" race-window files.
Fixed Ctrl+Z in the code editor undoing through into a previously-edited file's history.
Fixed Ctrl+Enter (quick-run) letting you stack up multiple scripts running "in the foreground" at once by mashing it repeatedly.
Fixed "Leverage"'s edit-a-script objective leading players to hunt for a "correct" destination file that never mattered.
Fixed loading a save being able to revert real progress to an old state after a silent Steam Cloud failure. Also now keeps one prior local backup on every save.
Fixed sleep() taking far longer than expected on upgraded CPUs after loading a save (up to ~8x longer on a fully-upgraded CPU).
Fixed deep or infinite recursion silently hanging instead of erroring - now fails cleanly with a real error.
Fixed sending #lounge messages faster than the previous send's round-trip silently losing the earlier message. Messages are now queued and sent in order.
Fixed connect(t, port)/smtp_connect(t) silently failing to resolve the right server when t came from parse_target().
Fixed user-defined functions running completely unthrottled, bypassing CPU-tick pacing and the CPU-overload kill switch - sleep() inside a function did nothing at all. Functions now pace correctly, just like the rest of a script.
Fixed cat /proc/cpuinfo on your own home box always showing "BD-CPU Mk.I" regardless of what CPU you'd actually bought.
Fixed publishing a script from inside a ~/scripts subfolder never actually reaching /community/ for subscribers.
Clarified connect()'s in-editor help text - success only means the port was reachable, not that your credentials were valid.
Fixed a script calling disconnect() never getting scored by a server's IDS the way disconnecting from the terminal already did. Both paths now apply the same log-based noise penalty.
Fixed cat/read/show (and anything redirected from them with >/>>) silently corrupting file content that contained certain characters.
Fixed help api missing 27 of the game's real scripting functions - pivot(), scrub_log(), upload(), resolve_hostname(), and more.
Fixed noise (the global detection meter) being wipeable to zero for free by connecting to any other server and back.
Fixed Bypass Source's shop page advertising a $700 purchase price when the header and the actual charge were both $650.
- Static
New
exit() - halts a script immediately, even from inside a function. return only pops back to whatever called that function; exit() stops the whole script right there, at any nesting depth.
raise("message") (alias: error()) - same idea, but marks the stop as an actual error instead of a clean finish. No try/except to catch it yet (that's a bigger project), just a clean way to abort on a bad condition instead of deliberately triggering an unrelated error to get the same effect.
import(expr) now also works as an expression - var lib = import("x") works now, not just the existing statement form (import(x) as alias). Usable anywhere an expression is valid: a var initializer, a function argument, inline.
touch(conn, path) / touch_local(path) - create an empty file if it doesn't already exist, and never overwrite one that does. write_file()/save() can create a missing file too, but they're not a safe substitute - calling either on a file that already exists destructively wipes its content to empty.
get_args() - returns every argument a script was called with, even ones beyond your declared @param list. Previously anything passed past the last @param was silently dropped.
my_files(path) - now takes an optional path (still defaults to your home directory) and returns the same rich {name, path, type, size} shape list_files() does remotely, so you can walk your own local filesystem recursively the exact same way. read_local() accepts a my_files() entry directly too, same as read_file() does with list_files() entries.
The browser now supports a few ways to hand-inject a vulnerable form directly, not just the scripted sqli() route.
Changes
sqli() and cmd_inject() no longer need a conn argument - they're plain HTTP checks against a web form, same as wget()/browse(), so requiring a connect() session first was never actually necessary. Just call sqli(url) / cmd_inject(url) directly now.
Font size setting now goes up to 48pt (was 20pt) - helps on some Linux/HiDPI setups where the terminal was unreadably small even at max size. Past 20pt (the old ceiling) the value gets a red \ tag next to it, since that's genuinely true - the UI has never actually been checked at those sizes. Go wild.
Various performance changes
Bug Fixes
Forkbomb (or a script reconnecting to an already-cracked target) could permanently disable tracing on that server - a check meant to make sure trace never gets skipped on your actual mission target was comparing server references directly, which silently stopped matching once that server got reset (by forkbomb, or by abandoning and restarting the mission). Fixed to check by IP instead, which can't go stale the same way.
A server could stop showing as CRACKED (and crack() would insist on cracking it again from scratch) after abandoning a mission, forkbombing, or just quitting and coming back - resetting the server's live state didn't touch your actually-remembered password in ~/.ssh/credentials, but nothing was checking that on the way back in. crack(), the network panel's status, and reconnecting now all recognize a password you already have.
Paths built with an extra slash (e.g. joining a path that already ends in /) could silently fail to resolve - even though the file existed under its normal path. Fixed at the source, so this can't bite any script, not just the default ones.
Some old saves still had ~/docs/api_quickref.txt pointing at a dead web address - a leftover from before launch that never got cleaned up in saves carried over from testing. Fixed for those saves too.
Publishing anything to the Steam Workshop failed with "submitItemUpdate failed (result=9)" - the preview image path only worked when run from the editor, not in the actual shipped game. This affected every publish, of every content type - should be properly fixed now.
touch/new always created the file on your home PC, even while connected to a remote server - unlike mkdir/mv/cp/rm, which already knew to target whichever server you're connected to. Fixed.
write_file() (scripting) didn't trigger mission objective checks the way editing a file interactively already did - a script-based edit could be completely correct and just never register as complete, since nothing ever re-checked. Fixed.
Adjusted some mission objectives to be less exact
Passing a list_files()/my_files() entry directly into read_file(), write_file(), delete_file(), download(), or read_local() never actually worked - it was reading the entry's bare filename (name) instead of its full path (path), which can't match a real path in any case. Fixed to use path.
A custom cracker built on hash_string()/hash_distance() (the "Crack Source" shop tools) could converge on a wrong-but-hash-matching password that failed to actually log in - two different characters could hash to the same value at some security levels, so a from-scratch coordinate-descent cracker had no way to tell it had the wrong one. crack() itself was never affected (it hands back the real stored password on success, not its own guess). Hashing is now collision-free at every security level, so a correctly-written custom cracker always recovers the real password.
Buying a hardware upgrade (CPU/RAM/disk/network) while a promo discount was active charged the discounted price and marked it "owned", but never actually applied the upgrade - the discounted purchase path skipped the code that applies the effect entirely. Fixed.
mv/cp on a remote server never checked your access list - unlike cat/download/edit/rm, which already do. A cracked low-privilege account could relocate or copy a file straight out of a directory it was never granted access to. Fixed.
Declining the quit confirmation ("Return to main menu? (y/n)" -> n) printed "Cancelled" but then also "Unknown command: n", and left the prompt stuck showing a bare > - the cancel branch was missing the same prompt-reset step every other y/n confirmation already has. Fixed.
A player's own messages sent from Discord (a linked account) never showed up in #lounge in-game - the dedupe that skips re-showing a message you just sent yourself matched on Steam ID alone, so it also silently ate anything posted from Discord under that same linked account. Fixed.
Typing @everyone/@here (or a role mention) in #lounge actually pinged the whole Discord server through the game-to-Discord chat mirror. - Mentions now render as plain text on the Discord side instead of resolving. Fixed.
#lounge timestamps showed the sender's raw UTC clock instead of your own local time - everyone saw everyone else's messages timestamped an offset off from their own clock. Now converted to your system's local timezone for display.
- Static
Bug Fixes
Imported modules couldn't use any builtin or API function inside their own functions - calling log(), int(), str(), connect(), or anything else from inside a function defined in an import'ed module failed outright with "'x' is not defined." Modules now have the same full builtin/API surface a top-level script already has.
The default grabber.bd script never actually worked against a real target - it called connect() without a username, so it never authenticated even right after cracker.bd had already cracked the account. Fixed to reuse the same parse_target() pattern cracker.bd already uses, so it picks up the credential you already have.
mv on a directory left its contents behind at the old path - the directory's own listing moved to the new location, but anything inside it stayed fully readable and runnable at the old path instead of moving with it. Directories now actually take their contents with them.
Interrupting a script with Ctrl+C while it had an open connection could leave you stuck - ps showed nothing to kill, and disconnect refused because something was still "connected," with no way out short of restarting. Fixed.
The Tidy-Up Job's first objective could complete before you'd touched the target server at all - an objective-checking bug let it match against an unrelated server's default configuration instead of the actual mission target.
Copying text out of #lounge chat could return the wrong thing on paste. Fixed.
- Static
Features
Recovery shell for a deleted root filesystem - if you (or a script) rm -r your own machine's / directory, the terminal now drops into a bare recovery> prompt instead of leaving you in a broken, half-working state forever. Only basic navigation works until you run restore, which reinstalls fresh from the recovery partition - wipes every local script, wordlist, and download back to factory defaults, permanently. Asks for confirmation first.
The same recovery gate now applies to remote servers - deleting a target's own / (yours or a stray script's doing) now shows the same recovery> prompt instead of silently leaving the box in a confusing broken state. forkbomb remains the way out, same as before, just properly signposted now.
grab_hash() now also returns security, and hash_string() takes an optional third security argument - the "Crack Source" SDK's local hash-matching technique (hash_string() + hash_distance() against a hash from grab_hash()) only ever worked against a zero-security demo account before this. It now works against real targets, the way the SDK's own documentation always claimed it did.
Wildcards in rm, ls, mv, and cp - rm -r ~/downloads/*.db, ls *.txt, cp *.log backup/ and friends all work now. mv/cp with a pattern matching more than one file require the destination to already be a directory, same as a rel shell.
New bitwise operators: &, |, ^, ~, <<, >> - and / or / xor / not / left-shift / right-shift on numbers. Precedence matches Python: comparison binds loosest, then |, ^, &, then shifts, then + / -.
grep -r <pattern> <path> - recursive search across an entire directory tree, not just a single named file. Uses real grep's pattern-first argument order for this form.
find <path> -name <pattern> - real filename search, finally its own command instead of a bare alias for grep. Supports * wildcards.
cd - - jumps back to whichever directory you were in before your last cd, and prints the path, same as a real shell. Tracked separately for your own machine and whatever server you're connected to.
echo - prints back whatever you give it, quotes optional.
Bug Fixes
Abandoning a mission with objectives already completed, then re-accepting it, could show everything as done while the mission itself could never actually complete - objective completion flags weren't reset on abandon, so the game skipped re-verifying them against the freshly-reloaded server. Now reset properly.
range() with extreme bounds (values near Int64's limits) could bypass its own safety cap entirely - the size check computed on ints, which silently overflow and wrap at that scale, so an oversized request could slip through and try to allocate a multi-quintillion-element list in one shot, hanging or crashing the game instead of erroring cleanly. Fixed at the root: the check now happens in float space before anything is cast back down to int.
rm -r / only ever deleted the / bookkeeping entry itself, not its actual contents - a path-prefix bug meant "deleting root" barely touched anything. Recursive deletes on the root directory now actually remove everything underneath it, on both your own machine and remote servers.
rm -rf (and other combined/reordered flags like -fr) weren't recognized at all - only an exact -r was handled, so -rf / was parsed as a literal filename and failed with "No such file". Flags are now parsed token by token, so -r, -f, -rf, -fr, -r -f, etc. all work.
kill -9 <pid> silently killed nothing - the whole argument was parsed as one integer, and "-9 5".to_int() stops at the space and returns -9, so it looked for (and never found) process -9 while the real pid went untouched. Signal flags are now stripped and ignored - there's only one way to end a process here, not real signal semantics.
- Static
Features
Music, for real this time - the music command is live: view the playlist, music next / music prev to skip around, music <name> to jump straight to a track, or music set <name> to loop one track on repeat instead of rotating, for anyone who just wants one song. Tracks now play out in full - around four minutes each, matching the standalone soundtrack - and fade out naturally before handing off to the next one, instead of the old hard cut on a fixed timer.
forkbomb - a last-resort recovery command for a remote server you've broken beyond normal use (accidentally deleted its own /, or a stray script wrote a file literally named after a directory path). Crashes the target and restores it fresh from backup, undoing every change you've made to it - deleted files, uploads, cracked users, all of it. Asks for confirmation first. Not an actual attack - it's a reset button, framed as one.
New: get_cpu() - returns your current aggregate CPU load (0-100) across every running process, the same number your own status bar shows. Scripts can now self-throttle (if get_cpu() > 80: sleep(0.5)) instead of finding out about overload only when everything gets killed.
New: object.remove(key) - removes a property from an object, joining keys()/values()/has(). Objects previously had no way to delete a key at all.
creds - a formatted view of every cracked username/password, grouped by server. Reads the same ~/.ssh/credentials file cat already showed you, just organized - no more tracking it yourself.
Bug Fixes
Confirming a completed mission in IRC could silently fail to advance it - replying "yes" when an NPC asked for confirmation crashed partway through an internal check on almost any mission (a leftover call meant for a plain dictionary, used on a typed object instead), so the mission never moved to its upload step or finished, even though every objective was already checked off. This was a serious one - by far the most impactful fix in this patch.
Two "delete/scrub" mission objectives could become permanently unsatisfiable - clearing /var/log/auth.log (or any other common path) only counted if it was gone from every server you'd ever registered, not just the mission's own target. On a path that common, some other server almost always still had a copy, so the objective could never complete. It now only checks the mission's own server(s), and multi-server missions can point a specific objective at one server directly.
Abandoning mission_00 or a Daily/Weekly Audit and then finishing something else could corrupt that mission's completion - the abandon handler only reset its "is this mission active" flag for the base Full Audit, not mission_00 or the Daily/Weekly variants. Finishing an unrelated mission afterward could get silently rerouted through that abandoned mission's completion logic instead of its own.
cp on an encrypted or protected file dropped both the encryption and protection on the copy - cat-ing the copy showed the plaintext immediately, no cracking required, defeating any encryption-gated file. The copy now carries over encryption and protection state, same as the original.
Browser back button could leave you interacting with the wrong server - after following a link to another server and pressing back, the page shown reverted correctly, but the browser's internal "current server" pointer didn't - so the next form submit or login on that page could hit the server you'd left, not the one on screen.
mkdir foo/ (trailing slash) silently created an unreachable phantom directory, and mkdir .. created a literal folder named .. instead of failing sensibly - mkdir's path handling never normalized paths the way every other file command already does. Also fixes the same family of bug in mv/cp/grep/mkdir, where a filename that happened to start with ~ (but wasn't a home-folder shorthand) got mangled instead of treated literally.
/join//leave in IRC required exact-case channel names while /msg and /whois didn't - /join #Blackdoor failed even with #blackdoor open. Now case-insensitive like its siblings.
exit claimed nothing was connected even when a background script still held a connection - it skipped the same explanation disconnect already gives in that situation, which points you at ps/kill <pid> instead.
decrypt(conn) (missing the required path argument) hit a raw engine error instead of the usual decrypt: ... message every other function in the API gives for bad input.
List/string indexing didn't support negative indices (list\[-1], "abc"\[-1]) even though pop()/insert() already did and the v0.11.0 notes said it should - fixed to match.
neofetch, sysinfo, credits, and fortune are real, working commands that were missing from tab completion and typo suggestions. Added.
.size() on lists and strings was implemented but missing from the editor's built-in cheatsheet. Added.
upload(conn, local_path, "/some/dir/") (trailing slash, no filename) wrote a file literally named after the directory instead of appending the source filename - the directory check didn't strip the trailing slash first, so it silently missed the stored directory entry. Same bug already fixed for scp in v0.11.0, just never applied to the scripting API's own upload().
The Mission and Network panes could go blank after a live layout.cfg edit (or loading a save with a custom layout) and stay blank until quitting to the menu and back - both panes only render their own content once, the first time they're built; every later panel rebuild reuses the same control as-is instead of re-rendering it. Now re-rendered every time panels rebuild, not just the first.
A side mission's crack objective could never complete - side_gains_and_losses.json used "user": "cto" instead of "target": "cto", a field the loader doesn't read, so the crack check silently checked against an empty username forever.
A side mission's log-scrub objective could become permanently unsatisfiable - same bug class as the delete fix above, just not applied to scrub_log: with no server specified, it checked every registered server's log for the player's IP instead of just the mission's own target.
Several servers listed filesystem entries at the root level (lib, run, sbin, sys) that didn't actually exist - ls / would show them, but exploring any of them failed. Added the missing entries.
The shop mislabeled its own leaderboard server's IP, and referred to one of its own products by an old, unused name in three different places on its own pages - both now consistent with what's actually for sale.
Upload mission objectives only checked that a file existed at the destination, not what was in it - writing any file with the right name to the drop server completed the objective, real evidence or not. Now checks the actual file content against the source you were meant to deliver; the destination filename no longer has to match at all, only the content.
- Static
Features
Discord Rich Presence - the game now shows what you're up to in Discord: connected to a server, on a mission, or freelancing in the terminal.
Live chat content filter - messages sent in #lounge are now automatically filtered for inappropriate language.
Steam Workshop publishing, for real this time - publish <scriptname> now opens a proper in-browser form instead of terminal prompts: pick Script or Tool, write a real multi-line description, toggle Content tags (Hacking, Puzzle, Utility, Fun). Missions and servers published from the Workshop editor get the same tag picker, plus an auto-derived Difficulty tag from their existing difficulty/security stats, and servers gained a description field to match missions. Every published item now also gets a default .bd preview icon instead of shipping with none.
Linux build now exists - early days, still shaking out testing, but a Linux build is now being built alongside Windows. Treat it as experimental for now - report anything weird you hit.
API Changes
probe_vuln() no longer lowers security (breaking change) - it's now a pure, repeatable check: calling it again returns the same vulnerability until it's actually exploited. A new exploit_vuln(conn) does the real work - chip away at the current vulnerability over several calls (same progressive model as bypass_firewall()), applying its security reduction on the call that finishes it. Probing first makes exploiting faster. If your script called probe_vuln() expecting it to weaken security, switch it to exploit_vuln().
Bug Fixes
Trace countdown blinking too early - the terminal header would start pulsing well before the trace was actually urgent. It now shifts smoothly from cyan to red as time runs out, and only blinks in the final 10 seconds.
\ tag mislabeling in chat - some demo players were occasionally appearing without the \ tag. Fixed.
/whois marvelous - marvelous was missing a whois entry like the other #lounge regulars. Fixed.
print() now actually works - previously calling print() in a script errored out with a misleading "did you mean 'int'?" instead of pointing to log(). It now just works as an alias for log(), along with println(), puts(), echo(), and write().
Saving in the editor flashed red - pressing Ctrl+S showed the same red flash used for the unsaved-changes warning, even on a successful save. It now flashes green. Also fixed a rare case where saving and exiting the editor in quick succession could leave the header stuck amber.
Subscribed Workshop scripts not appearing on the drop server - /community/ only refreshed at specific early-game story beats, so subscribing to something new on an existing save had no way to show up. It now refreshes on every boot if the drop server is already registered.
Four side missions couldn't be completed (Chicken Run, Milk Argument, Insurance Fraud, Paper Chase) - each mission's upload objective expected a filename that didn't match what you'd actually have after downloading the file, so the upload always "succeeded" but the objective never triggered. If you got stuck on one of these with everything seemingly done, this was why - fixed for all four.
A failed connect() could still let you crack() (or download/upload/etc.) whatever you were previously connected to - if connect() failed (bad hostname, typo'd target), scripts that carried on and called crack(), list_files(), download(), and similar functions right after weren't checking that the connection actually succeeded - they'd silently operate on your last real connection instead of erroring out. Fixed across the whole API; a failed connection is now properly rejected everywhere.
- Static
Features
#lounge -> Discord relay - messages sent in the in-game #lounge IRC channel now appear in the Blackdoor Discord server in real time. One-way for now (Discord -> game coming later).
network list - lists every server you have ever cracked, even if you have since removed it from the network graph. Use store <ip> to re-add one.
graybrd has been busy - a new server has appeared on the network. No further details.
marvelous joins #lounge - new community NPC. Tinkerer. Very into scripting. Already found an infinite loop bypass. Welcome.
Online player count - the IRC pane header shows how many players are currently in #lounge.
New #lounge chatter - a few new conversation threads from the community Discord.
ProxyChain rework (full game only) - bounce() has been replaced with pivot(conn). Pass an active authenticated connection and pivot() establishes a relay through that server and drops the connection - future traffic routes through it. Chain multiple hops by calling pivot() again on the next box. pivot_clear() wipes the chain. The CLI pivot clear command does the same from the terminal. Right-clicking any IP in the network pane now shows Add to pivot chain / Remove from pivot chain when ProxyChain is purchased.
Bug Fixes
local_crack() didn't authenticate on already-cracked users - if you called local_crack() on a user that had already been cracked, it returned success but didn't log you in. Subsequent filesystem calls would fail with an auth error. Fixed.
crack() now writes to ~/.ssh/credentials and ~/.ssh/known_hosts - previously only the terminal SSH command stored credentials after a successful crack. The crack() API now does the same, so scripts that crack a box will automatically populate your credentials and known_hosts files - no manual bookkeeping needed.
connect() now accepts ~/.ssh/known_hosts as third argument - if the target server's IP is listed in your known_hosts file and the user has already been cracked, connect() will authenticate in the same call. Pairs naturally with the above: crack a box once, and future scripts can reconnect with just connect("root@host", 22, "~/.ssh/known_hosts").
In-game browser IP navigation - clicking an IP address link in a website now loads that server in the browser correctly. Previously it was treated as a relative path and appended to the current URL.
Browser history preserved across server hops - navigating to a different server via a link no longer clears your browser history. Back still works.
Extensionless browser links now resolve correctly - links without a file extension now try .html before falling back to /index.html, matching standard web server behaviour.
Pivot chain now requires a cracked server - right-clicking "Add to pivot chain" on a server you haven't cracked now shows an error instead of silently adding it. Adding to the chain is also blocked while a trace is active.
Backspace triggered browser back while typing in IRC - pressing backspace in the IRC input would navigate the browser backwards instead of deleting a character. Fixed.
Script errors left trace timer running - if a script errored out after calling connect(), the connection was never cleaned up and the trace timer kept ticking in the background. Fixed.
Escape in the editor now dismisses autocomplete first - pressing Escape while the autocomplete popup was open would skip past it and trigger the editor exit warning. Escape now closes the popup first; a second press shows the unsaved changes warning, and a third exits. The warning also flashes the header bar red so it's harder to miss.
print() now suggests log() - calling print() in a script now gives "Did you mean 'log'?" in the error rather than just "not a function".
/help command in IRC - typing /help in any IRC channel now lists the available IRC slash commands.
Mission 00 (The Interview) softlock - probing after cracking (without probing first) now correctly triggers objective completion. Also fixed a separate issue where loading a save mid-mission could leave objectives permanently stuck and the mission unfinishable.
- Static
Current Release
Build 25422119
Uploaded Oct 10, 2026
System Requirements
How to Install
Blackdoor.exe to play
Troubleshooting tips
• Run Redist/_CommonRedist installers if game won't start
• Add folder to Windows Defender exclusions
• Run as administrator
Download
Direct link available
Needed when extracting (WinRAR / 7-Zip) — not a site login.
8DB176A7
7316bc91a316a355a85b1013374d106426ad9edaf28ec70e8fa42602b603c7bf
You May Also Like
View AllDownload Blackdoor for PC with a direct link or via torrent. Get the full version of Blackdoor for free. Blackdoor is a Indie released by Static Signal.