CLI tool
iximiuz/shellgym avatar
iximiuz/shellgym

iximiuz/shellgym grades reps from outside the shell, which is why it wants root on a disposable box

ShellGym - an Interactive Linux Command-Line Trainer

428 stars38 forksGoNOASSERTION

At a glance

What is it?
ShellGym is a Go daemon with a web UI that watches a plain Linux terminal through procfs, the kernel proc connector, and plain state checks, then marks a rep done when the system changes. Nothing is injected into your shell, but the daemon runs as root and expects a host you can throw away.
Who is it for?
ShellGym is worth the setup cost for anyone training file tree, redirection and signal reflexes on a box they do not care about, since its whole value is that a rep is graded on real system state rather than on a simulated answer key. Take the disposable host advice literally: root plus live actions plus kill-process and free-port tasks is a combination that will hurt a machine with anything on it.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 3, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The daemon runs as root on a host you are meant to discard

The start command is a sudo invocation, and the reason is written into the caution block next to it. Since reps will ask you to perform real actions on the live system, the instruction is to use ShellGym only with a disposable Linux host. Four routes are offered: a local VM through Lima, SlicerVM, VirtualBox or similar; an iximiuz Labs Linux Playground; or a paid host such as a DigitalOcean droplet or an EC2 instance. A plain Linux host is all that is required, described as a VM, a spare laptop, or an EC2 instance, and the compatibility claim is deliberately hedged: it should work on most, if not all, mainstream Linux distributions.

The sample tasks explain why root is unavoidable. Each rep asks for one concrete action, and the named set includes entering a directory, creating a file, killing a process, and freeing a port. None of those work without privileges, and the daemon also needs to read other users' process state.

There is also a hosted path with none of that setup. The ShellGym Playground is described as a regular Ubuntu VM with ShellGym and Go preinstalled and the split-screen view already arranged, available after a free sign-up with GitHub. That route is the cheapest way to find out whether the idea holds up, and it also removes the question of what is listening on your machine, since the daemon there is not the one on your laptop.

Nothing is injected into your shell, so nothing has to trust it

The design constraint that shapes everything else is stated as a negative: the student's shell is not modified in any way, with no prompt hooks, no wrappers, and no special shell functions. All observation happens from the outside, which means you work in the regular Linux terminal and the skills you drill transfer one to one to any real terminal.

Three mechanisms carry that observation. The first is procfs, used for discovering the interactive shells, reading their working directories, and scanning processes, files and ports. The second is the kernel proc connector, described as a netlink firehose of every exec() on the box, which is how the trainer notices which commands the user runs. The third is plain state checks: files existing, processes running, ports listening.

The split between those two observation channels is what makes the design coherent. Command history comes from the exec stream, so the trainer can tell that a command ran without needing a shell hook to report it. Completion does not. A rep finishes the moment the system state changes, which means the decision is made by a procfs scan or a direct check rather than by anything the user typed. The docs point to docs/detection.md for how each mechanism works, and docs/design.md for the architecture, with a student guide and an authoring guide alongside them.

One daemon, one path, and a fixed local port

Starting the daemon takes a path and a user name, and the address is not part of the quick start command at all:

sh
sudo ./shellgym serve --path "$PWD/paths/sample-linux-101" --user $USER

The web UI then lives on a fixed loopback port:

sh
open http://127.0.0.1:63636

So the daemon has one path per process, stated as a rule rather than a limitation to work around: one daemon serves one path. Two curricula in parallel means two daemons, and nothing in the quick start shows how the second one would be given a different address.

The Makefile is where that address appears. Its run target passes it explicitly:

sh
sudo ./bin/shellgym serve --path paths/sample-linux-101 --addr :63636 --user ${USER}

The same target carries a comment restricting it to playground use only, which reads oddly next to the quick start that has you run the equivalent command by hand. Running it detached is a systemd unit rather than a daemon flag:

sh
sudo systemd-run --unit=shellgym --collect \
    "$PWD/shellgym" serve --path "$PWD/paths/sample-linux-101" --user $USER

The --collect flag keeps the transient unit from piling up in the journal, and the daemon is expected to survive reboots anyway, since progress lives on disk.

The one-line install guesses your CPU with sed

The release route is a single line that derives the architecture and pipes the archive straight into tar:

sh
arch=$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')
curl -L "https://github.com/iximiuz/shellgym/releases/latest/download/shellgym_linux_${arch}.tar.gz" | tar xz

Two details are worth noticing. The sed rule maps only x86_64 and aarch64 onto the names Go tooling uses, so any other uname output passes through unchanged and produces a URL for a file that will not exist. And the archive unpacks into the current directory with no target folder, which is why the source route creates a symlink instead of leaving a nested path.

The source route is two steps, and it needs Go:

sh
make build
ln -s bin/shellgym shellgym

The Makefile builds with CGO_ENABLED=0 and writes to bin/shellgym, so the symlink exists purely to make ./shellgym resolve as the quick start expects. The release tarball bundles both the binary and the sample learning path, while a source build gives you the paths directory from the checkout anyway.

Both routes land on the same three targets that matter. make validate builds first and then lints the reference path, make test runs go test ./... with a 300 second timeout, and make vet runs go vet. A clean checkout can therefore be checked before anything is started with root.

The download URL says latest, and the release line it points at is still pre-1.0: v0.0.12 and v0.0.13 are both dated 2026-09-15, with v0.0.14 the next day on 2026-09-16, and the branch itself was last pushed on 2026-10-01. A user of the latest URL is therefore running a build that predates the newest commits. Licensing is also uneven: a LICENSE.md file sits at the repository root while the license field for the project is left unresolved, so read the file rather than assume which terms it grants.

The module path points at a different repository than the code lives in

go.mod declares the module as github.com/iximiuz/labs-content/tools/shellgym, while the repository it ships in is iximiuz/shellgym. That mismatch is the kind of detail that stops a go get from working, since the import path a consumer would guess from the URL is not the one the module answers to. Anyone integrating this has to use the module path as written rather than the repository path.

The dependency set is small and each entry maps to one visible feature. cobra v1.8.0 provides the command line, with pflag and mousetrap pulled in indirectly. goldmark v1.7.1 is the markdown parser, which matches a format where units are markdown pages with YAML frontmatter. gorilla/websocket v1.5.1 carries the live UI channel between the browser and the validation engine, and creack/pty v1.1.21 is the pseudo-terminal used by the solve command, which types reference solutions into a real pty shell. yaml.v3 v3.0.1 parses path.yaml and the frontmatter, and golang.org/x/sys v0.19.0 with x/net v0.17.0 are transitive. The language floor is go 1.22.

The build is static, with CGO_ENABLED=0 set in the Makefile rather than in any environment file, and the release pipeline is configured from a .goreleaser.yaml at the repository root. That is what produces the shellgym_linux_ prefixed tarball names the download URL expects.

Resuming a path works only if nothing else touched the machine

Progress is persisted on disk, in /var/lib/shellgym by default, and the promise is that the user can stop at any time and resume days later, surviving daemon restarts and reboots of the box. Randomized parameters are sticky per attempt, so a half-done unit looks the same after a resume: the directory names, tokens and ports a rep generated are not dealt again mid-attempt.

The caveat is stated in the same place and it is the interesting part. Because most learning paths expect the student to modify the state of the live system, such as creating files, starting processes and setting environment variables, a successful resume depends on the preservation of the system's state. If another process removes or modifies files the learning path requires, the restart can break and the progress will be reset.

So the durability claim and the reset risk are the same feature seen from two sides. A rep that asks you to free a port is durable precisely because the state that proves it is still there, and fragile for the same reason. The unit format leans into this: units can depend on the state left behind by earlier units, and can be filtered by distro or host capabilities, which makes an ordering assumption that a stray cleanup script or an unrelated service can break.

Paths are YAML and markdown, and four subcommands cover the authoring loop

ShellGym defines a format, not a curriculum, and the bundled sample-linux-101 directory under paths/ is the reference implementation. The layout is a directory tree where the numeric prefix on each level defines order:

code
paths/<path>/              # path.yaml: id, title, user
    010.module-a/          # numeric prefix defines order
        module.md          # optional module intro (static)
        010.unit-x/
            unit.md        # a single "rep" (tasks + checks)
    020.module-b/          # another module
        ...

A path is the whole course and is made of path.yaml plus modules. A module is a themed group of units with an optional static intro scene in module.md. A unit is one rep, a markdown page whose YAML frontmatter defines setup scripts, verification tasks, hints and a hidden reference solution kept for testing. A task is one verifiable condition, and the unit completes when all of its tasks are met. Beyond that fixed skeleton, units can be parametric with randomized directory names, tokens and ports, can come in variants where a different rep occupies the same slot and is drawn per attempt, can depend on state left by earlier units, and can be filtered by distro or host capabilities. The full format reference is docs/authoring-guide.md.

Four subcommands cover the work around that. serve loads a path, runs the validation engine and serves the web UI. validate lints and renders a path without running it, which is what the Makefile validate target calls. solve auto-types reference solutions into a real pty shell, simulating a student pass, which is the cheapest way to check that a hand-written path is actually completable. skills prints embedded authoring guides for AI-assisted content work, and a skills/ directory sits at the repository root next to a .claude/ directory. The dist target exists for the playground rather than for users: it assembles a self-provisioning bundle from e2e/playground.yaml that downloads https://labs.iximiuz.com/__static__/shellgym-dist.tar.gz, a file the comment says to publish after building.

Editorial conclusion

ShellGym is worth the setup cost for anyone training file tree, redirection and signal reflexes on a box they do not care about, since its whole value is that a rep is graded on real system state rather than on a simulated answer key. Take the disposable host advice literally: root plus live actions plus kill-process and free-port tasks is a combination that will hurt a machine with anything on it. Before a second session, check what the last one left running and keep the daemon inside one learning path, because the state a rep depends on is not protected from other processes.

Frequently asked questions

Does ShellGym need root, and what machine should I run it on?

The daemon is started with sudo, and since reps ask for real actions on the live system the instructions say to use ShellGym only with a disposable Linux host, such as a local VM, a Labs Linux Playground, a DigitalOcean droplet or an EC2 instance.

How does ShellGym observe commands without touching my shell?

No prompt hooks, wrappers or shell functions are installed. Commands are seen through the kernel proc connector as a netlink stream of every exec(), while completion is judged by procfs scans and plain state checks on files, processes and ports.

Where is ShellGym progress stored and when does it get reset?

Progress is persisted in /var/lib/shellgym by default and survives daemon restarts and reboots. It can be reset if another process removes or modifies files the learning path depends on, because resuming depends on the live system state being preserved.

What does the shellgym validate command do?

validate lints and renders a learning path without running it. The Makefile wires it to the bundled reference path, running shellgym validate --path paths/sample-linux-101 after a build.

What does shellgym solve do in ShellGym?

solve auto-types the reference solutions into a real pty shell, simulating a student pass so an authored path can be checked without typing it by hand.

What structure does a ShellGym learning path have?

A path is a directory tree with path.yaml holding id, title and user, modules named with a numeric prefix that defines order, an optional module.md intro, and unit.md files that are single reps with tasks and checks. One daemon serves one path.

Official sources

  1. Issues
  2. iximiuz/shellgym on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/iximiuz-shellgym.svg)](https://hysenlabs.com/projects/iximiuz-shellgym)