This document is a complete specification for implementing the remaining parts of the LinuxMissions game — a terminal-based Linux learning game modeled after k8smissions and dockermissions.
LinuxMissions is a CLI game that drops users into real Linux scenarios. Each level:
- Sets up a sandbox under
/tmp/linuxmissions/<module>/<level>/ - Presents a mission brief (story, objective, XP, difficulty, concepts)
- Lets the user run any shell commands
- Auto-validates after every command using a
validate.shscript - Awards XP and shows a debrief on success
- Earns a certificate upon completing a full module
The game runs as a Python TUI (using rich) that wraps real shell execution.
All engine files are complete and working:
| File | Purpose |
|---|---|
engine/engine.py |
Main game loop, level loading, command dispatch, validation |
engine/ui.py |
Rich-based UI: banners, panels, mission briefings, debriefs |
engine/player.py |
Player name prompt |
engine/reset.py |
Sandbox teardown + setup.sh runner, sandbox path = /tmp/linuxmissions/<level_id>/ |
engine/safety.py |
Block destructive commands (rm -rf /, fork bomb, etc.) |
engine/certificate.py |
Module completion certificate generator + saver |
| Module | Levels | Status |
|---|---|---|
module-1-filesystem |
10 levels | ✅ Complete |
module-2-text |
8 levels | ✅ Complete |
module-3-permissions |
8 levels | ✅ Complete |
module-4-processes |
2/6 levels |
play.sh— launcher scriptinstall.sh— dependency installerprogress.json— initial empty progress filelevels.json— level registry (auto-generated byscripts/generate_levels.py)scripts/generate_levels.py— script that walksmodules/and buildslevels.json
Every level directory under modules/<module-name>/<level-name>/ must contain:
mission.yaml # JSON (despite .yaml extension) — level metadata
setup.sh # Called with $1=sandbox_path; creates the broken state
validate.sh # Called with $1=sandbox_path; exits 0=pass, 1=fail
hint-1.txt # First hint (shown on first `hint` command)
hint-2.txt # Second hint
hint-3.txt # Third hint (most explicit / full solution)
debrief.md # Markdown shown after completing the level
{
"name": "Human-readable level title",
"description": "One paragraph story context",
"objective": "Exactly what the player must do",
"xp": 100,
"difficulty": "beginner | intermediate | advanced",
"expected_time": "5m",
"concepts": ["cmd1", "cmd2"],
"module": "module-4-processes",
"level": "level-3-background-jobs"
}#!/bin/bash
# $1 = absolute sandbox path (e.g. /tmp/linuxmissions/module-4-processes/level-3-background-jobs)
SANDBOX="$1"
# Create files, dirs, start background processes, set permissions, etc.
# All files MUST be created under $SANDBOX#!/bin/bash
# $1 = absolute sandbox path
SANDBOX="$1"
# Print ✅ PASS: ... on success, ❌ FAIL: ... on failure
# Exit 0 = level complete, Exit 1 = not yet- Start with
# Level Name - Section
## What you practiced - Section
## Commands to rememberwith a fenced code block - Optional
## Key insightor## Security noteor comparison table
- Startup:
play.shactivates venv →python3 -m engine.engine - Level load: reads
levels.json, finds first uncompleted level in selected module - Sandbox setup:
engine/reset.py:prepare_level()→ runssetup.sh $sandbox - Prompt loop: user types commands, engine runs them via
subprocess.run(cmd, shell=True, cwd=sandbox) - After each command: engine calls
validate.sh $sandbox→ exit 0 triggers level completion - Meta-commands handled by engine (not passed to shell):
hint,reset,skip,status,objective,help,quit - On completion: XP added, debrief shown, progress saved to
progress.json - Module completion: certificate generated to
completion/certificate-<module>.txt
Module directory: modules/module-4-processes/
Already done: level-1-ps-find, level-2-kill
Name: The Background Worker
Description: A data processing script takes 30 seconds. You can't wait — you need to run it in background and check on it.
Objective: Run process.sh in the background, check its status with jobs, bring it to foreground, then let it finish
XP: 150
Difficulty: intermediate
Concepts: &, jobs, fg, bg, Ctrl+Z, disown
setup.sh:
- Create
$SANDBOX/process.sh— a script that sleeps 15 seconds then writes "done" to$SANDBOX/result.txt - chmod +x it
validate.sh:
- Pass if
$SANDBOX/result.txtexists and contains "done"
hints:
- Run in background:
bash $SANDBOX/process.sh & - Check jobs:
jobs— see running jobs. Usefgto bring it foreground, or just wait. - The script writes result.txt when finished. Check:
cat $SANDBOX/result.txt
debrief.md:
- Explain
&,jobs,fg %1,bg %1,Ctrl+Z(suspend),disown(detach from terminal) - Table: job control commands
Name: Play Nice
Description: A CPU-intensive batch job is slowing down the web server. Launch it with a lower priority.
Objective: Run heavy.sh with niceness 15, save its PID to pid.txt, verify it's running with adjusted priority
XP: 150
Difficulty: intermediate
Concepts: nice, renice, ps -o ni, priority
setup.sh:
- Create
$SANDBOX/heavy.sh— infinite loop:while true; do :; done - chmod +x
- Create
$SANDBOX/READMEwith instructions
validate.sh:
- Check
$SANDBOX/pid.txtexists - Read PID from pid.txt
ps -o ni= -p $PID— check niceness is >= 10- Kill the process after checking
hints:
nice -n 15 bash $SANDBOX/heavy.sh &- Save PID:
nice -n 15 bash $SANDBOX/heavy.sh & echo $! > $SANDBOX/pid.txt - Verify:
ps -o pid,ni,cmd -p $(cat $SANDBOX/pid.txt)
debrief.md:
- Explain niceness scale: -20 (highest priority) to 19 (lowest)
nicefor new processes,renicefor existing- Only root can set negative nice values
Name: Signal Relay
Description: A daemon is configured to reload its config when it receives SIGHUP. Simulate this.
Objective: Send SIGHUP to the listener.sh process, which will write 'reloaded' to reload.log
XP: 200
Difficulty: intermediate
Concepts: kill -HUP, signals, SIGHUP, trap, kill -l
setup.sh:
- Create
$SANDBOX/listener.sh:(use actual sandbox path in the trap, interpolated during setup)#!/bin/bash trap 'echo "reloaded" >> "$SANDBOX/reload.log"' HUP echo $$ > "$SANDBOX/listener.pid" while true; do sleep 1; done
- chmod +x and launch:
bash $SANDBOX/listener.sh &
validate.sh:
- Check
$SANDBOX/reload.logexists and contains "reloaded"
hints:
- Find the PID:
cat $SANDBOX/listener.pidorpgrep -f listener.sh - Send SIGHUP:
kill -HUP $(cat $SANDBOX/listener.pid) - Verify:
cat $SANDBOX/reload.log
debrief.md:
- List common signals with real-world use cases
- Explain
trapin bash kill -lto list all signals
Name: Zombie Apocalypse
Description: A poorly coded parent process left zombie children behind. Identify them and clean up.
Objective: Find all zombie processes, write their PIDs and names to zombies.txt, then kill the parent to reap them
XP: 200
Difficulty: advanced
Concepts: ps, zombie, Z state, PPID, kill parent, wait
setup.sh:
- Create
$SANDBOX/make_zombies.sh:#!/bin/bash # Creates zombie children by not calling wait for i in 1 2 3; do (exit 0) & # child exits immediately, parent never waits done echo $$ > "$SANDBOX/parent.pid" sleep 3600 # parent stays alive, children become zombies
- chmod +x and launch it
validate.sh:
- Check
$SANDBOX/zombies.txtexists and has content - Check the parent process is no longer running (or zombies are gone)
hints:
- Find zombie processes:
ps aux | grep Z - Zombies show 'Z' in STAT column. Get their PPID:
ps -o pid,ppid,stat,cmd | grep Z - Kill the parent to reap zombies:
kill $(cat $SANDBOX/parent.pid)then save info first
debrief.md:
- What a zombie is (exited child, parent hasn't called
wait()) - They use a PID slot but no memory/CPU
- Fix: kill parent (init/systemd adopts and reaps) or fix code to call
wait()
8 levels — teach bash scripting from first principles
| Level | Name | Objective | Key Concepts |
|---|---|---|---|
level-1-variables |
Speak Variable | Write a script greet.sh that takes a name as $1 and prints "Hello, $1!" |
variables, $1, echo, script args |
level-2-conditionals |
The Gatekeeper | Write check_file.sh $1 that prints "EXISTS" or "MISSING" depending on whether $1 exists |
if/else, -f, -d, test |
level-3-loops |
Bulk Renamer | Write rename.sh that renames all .txt files in a dir to .bak using a for loop |
for loops, glob, mv, basename |
level-4-functions |
DRY Deploy | Refactor a repetitive deploy script to use functions | function, local, return, $? |
level-5-input |
Config Parser | Write a script that reads key=value lines from a file and exports them as env vars |
while read, IFS, export, source |
level-6-error-handling |
Fail Loudly | Add set -euo pipefail and proper error handling to a broken script |
set -e, set -u, set -o pipefail, trap ERR |
level-7-subshells |
Parallel Launch | Use subshells to run 3 tasks in parallel and wait for all to finish | ( ), &, wait, subshells |
level-8-heredoc |
Template Engine | Use heredoc to generate a config file from variables | heredoc, <<EOF, cat, envsubst |
Validation patterns for this module:
level-1: Runbash $SANDBOX/greet.sh World→ check output contains "Hello, World!"level-2: Test both file-exists and file-missing caseslevel-3: Check.txtfiles gone,.bakfiles existlevel-4: Run the refactored script, check outputlevel-5: Source the config, check exported varslevel-6: Run script with missing var → should fail with clear message notunbound variablelevel-7: Check all 3 output files exist (created by parallel tasks)level-8: Check generated config has correct substituted values
8 levels — diagnose and work with Linux networking
| Level | Name | Objective | Key Concepts |
|---|---|---|---|
level-1-ip-addr |
What's My IP? | Find all network interfaces and their IPs, save to interfaces.txt | ip addr, ip a, ifconfig |
level-2-ping |
Can You Hear Me? | Ping 3 hosts from hosts.txt, record which are reachable in reachable.txt | ping -c1, exit codes, loops |
level-3-ports |
What's Listening? | List all listening TCP ports, save to listening.txt | ss -tlnp, netstat -tlnp, lsof |
level-4-curl |
API Call | Fetch a JSON response from a local test file using curl and extract a field | curl, -s, -o, jq, pipe |
level-5-dns |
Name Resolution | Look up the A record for hostnames in a list, save IPs to resolved.txt | dig, nslookup, host, /etc/resolv.conf |
level-6-netstat |
Connection Map | Find all ESTABLISHED connections and count unique remote IPs | ss -tn, awk, sort, uniq |
level-7-firewall |
Open Sesame | Using iptables or nftables rules file, identify which port is blocked (conceptual analysis) | iptables -L, rule reading |
level-8-tcpdump |
Packet Sniff | Use tcpdump to capture traffic to a file and identify the protocol | tcpdump, -w, -r, -i lo |
Notes:
- Networking levels use files to simulate (e.g., a pre-populated
hosts.txt, fake JSON files for curl) to avoid needing real network access level-4-curlusescurl file:///pathor serves a local filelevel-7is analysis-only (read the iptables output, answer a question)
8 levels — real sysadmin day-to-day tasks
| Level | Name | Objective | Key Concepts |
|---|---|---|---|
level-1-systemd |
Service Down | Enable and start a systemd unit file for a dummy service | systemctl start/enable/status, unit files |
level-2-journald |
Log Hunt | Find all log entries from the last hour containing "error" using journalctl | journalctl, --since, -p, grep |
level-3-cron |
Scheduled Sweep | Write a crontab entry that runs cleanup.sh every day at 3am | crontab -e, cron syntax, crontab -l |
level-4-users |
New Hire | Create user devuser, set password, add to docker group |
useradd, passwd, usermod -aG, id |
level-5-sudoers |
Controlled Power | Add a sudoers rule allowing devuser to run /usr/bin/systemctl without password |
visudo, /etc/sudoers.d/, NOPASSWD |
level-6-logrotate |
Log Discipline | Write a logrotate config for /var/log/myapp/*.log | logrotate config syntax, rotate/compress/daily |
level-7-env |
Env Surgery | A script fails because of missing env vars. Set them in .env and source it | export, source, .env, env, printenv |
level-8-at |
One-Shot Task | Schedule a one-time command to run in 1 minute using at |
at, atq, atrm |
Notes:
level-1-systemd: create a dummy unit file at$SANDBOX/myapp.service, copy to/tmp/location — validation checks systemctl if available, otherwise just checks unit file syntaxlevel-4-usersandlevel-5-sudoersrequire root — these levels should detect if running as root and skip gracefully if not, OR use a sandbox approach with mock files
6 levels — power-user text processing
| Level | Name | Objective | Key Concepts |
|---|---|---|---|
level-1-regex-grep |
Pattern Mastery | Extract all IPv4 addresses from a log using grep regex, save to ips.txt | grep -E, IPv4 regex, \d, groups |
level-2-awk-report |
Expense Report | Use awk to sum expenses by category from CSV, print sorted totals | awk arrays, END block, sort |
level-3-sed-multiline |
Config Injector | Use sed to insert a new line AFTER a specific pattern in a config file | sed /pattern/a, sed address ranges |
level-4-jq |
JSON Surgeon | Parse a JSON file, extract nested fields, filter an array | jq, ., [], select, map |
level-5-xargs |
Parallel Grep | Use xargs to run grep across 50 files in parallel, find the one with a keyword | xargs -P, -I{}, parallel execution |
level-6-column |
Pretty Table | Format a space-separated report file into aligned columns | column -t, printf, awk printf |
#!/bin/bash
# LinuxMissions launcher
clear
cd "$(dirname "$0")"
if [[ "$1" == "--reset" ]]; then
# prompt to reset progress.json (same pattern as k8smissions)
fi
if [ ! -d "venv" ]; then
echo "❌ Virtual environment not found. Run ./install.sh first."
exit 1
fi
export PYTHONPATH="$(pwd):$PYTHONPATH"
if [ -f "venv/bin/python3" ]; then
venv/bin/python3 -m engine.engine
else
echo "❌ Python interpreter not found inside venv"
exit 1
fi#!/bin/bash
# LinuxMissions installer
set -euo pipefail
echo "🚀 Installing LinuxMissions..."
# Check Python 3
if ! command -v python3 &>/dev/null; then
echo "❌ Python 3 required. Install it first."
exit 1
fi
# Create venv
python3 -m venv venv
venv/bin/pip install --upgrade pip -q
venv/bin/pip install -r requirements.txt -q
# Generate levels.json
venv/bin/python3 scripts/generate_levels.py
# Init progress.json if missing
if [ ! -f "progress.json" ]; then
echo '{"player_name":"","total_xp":0,"completed_levels":[],"current_module":"","current_level":"","module_certificates":[],"time_per_level":{},"level_start_time":null}' > progress.json
fi
echo "✅ Done! Run: ./play.sh"This script walks modules/ alphabetically, reads each mission.yaml, and produces levels.json:
#!/usr/bin/env python3
"""Walk modules/ and generate levels.json registry."""
import json
from pathlib import Path
from datetime import datetime, timezone
REPO = Path(__file__).resolve().parent.parent
MODULES_DIR = REPO / "modules"
OUTPUT = REPO / "levels.json"
modules = []
total = 0
for mod_dir in sorted(MODULES_DIR.iterdir()):
if not mod_dir.is_dir():
continue
levels = []
for lvl_dir in sorted(mod_dir.iterdir()):
if not lvl_dir.is_dir():
continue
mission_file = lvl_dir / "mission.yaml"
if not mission_file.exists():
continue
mission = json.loads(mission_file.read_text())
level_id = f"{mod_dir.name}/{lvl_dir.name}"
levels.append({
"id": level_id,
"name": lvl_dir.name,
"path": f"modules/{mod_dir.name}/{lvl_dir.name}",
"mission": mission,
})
total += 1
if levels:
modules.append({"name": mod_dir.name, "levels": levels})
OUTPUT.write_text(json.dumps({
"generated_at": datetime.now(timezone.utc).isoformat(),
"level_count": total,
"modules": modules,
}, indent=2))
print(f"✅ levels.json: {total} levels across {len(modules)} modules"){
"player_name": "",
"total_xp": 0,
"completed_levels": [],
"current_module": "",
"current_level": "",
"module_certificates": [],
"time_per_level": {},
"level_start_time": null
}The engine/ui.py module uses Rich. Here's how each screen looks:
- ASCII banner (BANNER constant in ui.py)
- Panel with player name, game description, keyboard shortcut hints
- Numbered list of modules with
done/totallevel count - Green ✓ for completed modules
- Player enters a number
- Title:
Mission N/Total: <name> DIFFICULTY +XP ~Xm - Body: description paragraph +
Objective:line +Concepts:dim line - Border color: blue
- Yellow border
- Title:
Hint N
- Green border
- Title:
Level Complete! +XP Xm Xs - Body: rendered Markdown from
debrief.md
- Dim border, simple table
- Player name, total XP, module progress N/M
- Saved to
completion/certificate-<module>.txt - ASCII art box with player name, module name, XP, date
>in bold green
[red]for blocked commands[yellow]for warnings
The sandbox root for any level is:
/tmp/linuxmissions/<module-name>/<level-name>/
Example:
/tmp/linuxmissions/module-4-processes/level-2-kill/
setup.shreceives this as$1and must create ALL files under itvalidate.shreceives this as$1and checks the state under it- The engine sets
cwd=sandboxwhen running player commands - The engine also sets
SANDBOX=<path>in the environment so player scripts can use$SANDBOX - Sandbox is rebuilt from scratch on
resetcommand
Recommended XP totals:
| Module | Levels | Suggested XP Range |
|---|---|---|
| module-1-filesystem | 10 | 100–150 each |
| module-2-text | 8 | 100–200 each |
| module-3-permissions | 8 | 100–200 each |
| module-4-processes | 6 | 100–200 each |
| module-5-scripting | 8 | 150–250 each |
| module-6-networking | 8 | 150–250 each |
| module-7-sysadmin | 8 | 150–300 each |
| module-8-advanced-text | 6 | 200–300 each |
Total: ~62 levels, ~10,000 XP
Each module should follow this arc:
- Levels 1–2:
beginner— basic command usage - Levels 3–5:
intermediate— combining commands, real scenarios - Levels 6+:
advanced— edge cases, security hardening, scripting
- Always check the primary objective first, print ❌ FAIL and
exit 1 - Check side effects (e.g., other files not accidentally broken)
- Final
echo "✅ PASS: ..."+exit 0 - Use
2>/dev/nullon commands that might fail for non-critical checks - For numeric comparisons use
-eq,-lt, etc. not= - For process checks, always handle missing
pid.txtgracefully
# Pattern 1: Create broken file
echo "wrong content" > "$SANDBOX/config.conf"
chmod 600 "$SANDBOX/config.conf" # too restrictive
# Pattern 2: Start a background process
bash "$SANDBOX/myloop.sh" &
echo $! > "$SANDBOX/myloop.pid"
# Pattern 3: Create directory structure with wrong permissions
mkdir -p "$SANDBOX/app/logs"
chmod 700 "$SANDBOX/app/logs" # too restrictive for app
# Pattern 4: Create a tar archive to extract
tar -czf "$SANDBOX/backup.tar.gz" -C "$SANDBOX/source" .
rm -rf "$SANDBOX/source"
# Pattern 5: Create a large set of files with some targeting
for i in $(seq 1 20); do
echo "line $i" > "$SANDBOX/file$i.txt"
done
echo "TARGET_VALUE=secret" > "$SANDBOX/file7.txt" # the one to findRecommended order to implement:
scripts/generate_levels.py— needed to test anythingplay.sh+install.sh— needed to run the gameprogress.json— initial empty file- Module 4 remaining levels (3–6) — finish the incomplete module
- Module 5 scripting (8 levels) — most self-contained to test
- Module 6 networking (8 levels) — use file-based simulation
- Module 7 sysadmin (8 levels) — some require root detection
- Module 8 advanced text (6 levels) — most complex, do last
After creating a level's files, test it manually:
# 1. Run setup
bash modules/module-X-foo/level-Y-bar/setup.sh /tmp/test_sandbox/
# 2. Inspect state
ls -la /tmp/test_sandbox/
# 3. Perform the solution manually
# (run the expected commands)
# 4. Run validate
bash modules/module-X-foo/level-Y-bar/validate.sh /tmp/test_sandbox/
# Should print ✅ and exit 0
# 5. Test failure case — don't do the solution, check it fails
bash modules/module-X-foo/level-Y-bar/setup.sh /tmp/test_sandbox2/
bash modules/module-X-foo/level-Y-bar/validate.sh /tmp/test_sandbox2/
# Should print ❌ and exit 1linuxmissions/
├── play.sh # launcher
├── install.sh # setup script
├── requirements.txt # rich>=13.0.0
├── progress.json # player save file
├── levels.json # auto-generated registry
├── SPEC.md # this file
├── README.md # user-facing docs
├── engine/
│ ├── __init__.py
│ ├── engine.py # main game loop
│ ├── ui.py # rich UI components
│ ├── player.py # name prompt
│ ├── reset.py # sandbox setup
│ ├── safety.py # blocked command filter
│ └── certificate.py # module cert generator
├── scripts/
│ └── generate_levels.py # builds levels.json
├── completion/
│ └── certificate-<module>.txt # generated on module completion
└── modules/
├── module-1-filesystem/ # ✅ 10 levels done
├── module-2-text/ # ✅ 8 levels done
├── module-3-permissions/ # ✅ 8 levels done
├── module-4-processes/ # ⚠️ 2/6 done — need levels 3–6
├── module-5-scripting/ # ❌ 8 levels needed
├── module-6-networking/ # ❌ 8 levels needed
├── module-7-sysadmin/ # ❌ 8 levels needed
└── module-8-advanced-text/ # ❌ 6 levels needed
Each level directory:
<level-name>/
├── mission.yaml # metadata (JSON)
├── setup.sh # creates sandbox state
├── validate.sh # checks completion
├── hint-1.txt
├── hint-2.txt
├── hint-3.txt
└── debrief.md
Total remaining: ~56 levels + 5 top-level files + 1 generator script