When I started building CoreQ Academy, a free cybersecurity training platform, I wanted beginners to practise Linux in the browser with zero setup. No VM, no Docker, no risk of breaking a real machine. So I built a Linux terminal simulator in TypeScript. Here's how it works and what I'd recommend if you build something similar.
Design goals
- Safe by construction. User input is never executed as code: no
eval, nonew Function, no server shell. - Deterministic. The same commands always produce the same output, so labs can be graded reliably.
- Realistic enough to teach. Permissions, users and groups, pipes, redirects, services and logs need to behave like the real thing.
Architecture
xterm.js -> line editor -> parser -> shell -> commands -> virtual filesystem + system state A virtual filesystem with real permission checks
Every node stores type, mode bits, owner and group. Reads go through a permission check, so cat /var/log/auth.log fails for a normal user exactly as it would on Linux:
can(node: FsNode, uid: number, op: 4 | 2 | 1): boolean { if (uid === 0) return true; // root const user = this.user(uid); const bits = node.uid === uid ? (node.mode >> 6) & 7 : user?.groups.includes(node.gid) || user?.gid === node.gid ? (node.mode >> 3) & 7 : node.mode & 7; return (bits & op) === op; } A parser that understands quoting
The parser turns a line into commands, arguments, pipes and redirects, handling single quotes, double quotes, escapes and $VARS. One subtle bug I hit: expr 6 '*' 7 was being glob-expanded into filenames. Real bash doesn't expand quoted wildcards, so the parser now marks quoted * and ? with sentinels and restores them after glob expansion.
Commands are pure functions over state
Each command receives the shell context (user, cwd, stdin, argv) and returns { stdout, stderr, code }. More than 200 commands are implemented this way, from ls and grep to ufw, sha256sum (a real SHA-256 in TypeScript) and a simulated git that warns when you commit something that looks like a secret.
State-graded labs
The part I'm proudest of: labs are graded on the final state of the system, not on matching one command. For example, a lab that asks you to fix a web app's configuration permissions checks:
- the file is no longer world-readable,
- the service account can read it,
- the service now starts successfully.
Any correct approach passes: chmod 640 plus chgrp, ACLs, whatever works. That's how real audits work too.
Persistence
The whole system state serialises to JSON and is saved in IndexedDB, so learners can close the tab and come back to their sandbox.
What I'd do differently
- Start with the parser tests. Quoting edge cases cause most of the bugs.
- Keep commands small. Many short, composable commands beat a few giant ones.
- Test on phones early. A terminal on a 390px screen needs care.
You can try it free at coreqacademy.com/labs/sandbox.
I'm Sean Ofodile, a cybersecurity leader, founder of Quantum Core Services and CIO of Seenu.io, an early-stage social discovery startup. I teach free cybersecurity classes at CoreQ Academy and on YouTube as Professor Sean Ofodile, and was featured on 11Alive in Atlanta with the Seenu team.
