_ _ _ _ _ _
| \| |___| |_| || |__ _ __| |_____ _ _ ___
| .` / -_) _| __ / _` / _| / / -_) '_(_-<
|_|\_\___|\__|_||_\__,_\__|_\_\___|_| /__/
An open effort to build the first program that can reliably win NetHack 3.6.6.
nethackers.dunnolab.ai · PyPI · Apache-2.0
NetHack (1987) is among the oldest unsolved challenges in games. NLE put it forward as a grand challenge for AI; it is still unsolved, and it is the hardest game in the BALROG suite. NetHackers is an attempt to find out — together — whether a coding agent can write the program that cracks it.
What gets scored is the program: a symbolic bot, cheap to run and — as long as you keep it deterministic — replayable. How you produce that program is entirely your business. Write it by hand, evolve it with a coding agent, or build a better thing that builds it — the hub never runs your search and never assigns you work.
Contents
- How it works
- Public and Private Dungeons
- Safety: this runs untrusted code
- Install
- Quickstart
- GitHub is the infrastructure
- Documentation
How it works
The unit of evaluation is one bot directory containing a bot.py with a
top-level make_agent(). That's the whole contract — see
docs/harness.md.
Objectives grid over the 73 legal starting identities (role-race-align-gender,
e.g. val-dwa-law-fem): NetHack 3.6.6's 38 valid (role, race, align) triples, each
crossed with both genders — except Valkyrie's 3, which the game locks to female.
35×2 + 3 = 73. Each identity has a published batch of 15 seeds. A bot is
scored on how far it gets, averaged over that batch; the north star is an
ascension, which nothing has managed yet.
The hub keeps the best program per identity (the elites). Anyone can pull an elite, improve it, and register the result — so one contributor's improvement becomes everyone's parent.
hub ──pull elite──▶ your machine ──mutate──▶ candidate
▲ │
│ evaluate
└──────────register (repo@commit)◀─────── (sandboxed arena)
Our own evolutionary harness — a coding agent in a container, MAP-Elites over
the 73 identities — ships in this repo. It is the worked example, not the
required path. docs/harness.md documents it as something to
read and reuse when building your own.
Public and Private Dungeons
Every program carries up to two scores, and they answer different questions.
| Public Dungeons | Private Dungeons | |
|---|---|---|
| Tier in the API/DB | self-reported |
verified |
| Seeds | 15 published per identity | secret, set per deployment (15 in production) |
| Who ran it | you, on your machine | our verifier, on our hardware |
| Reproducible by you | yes, given a deterministic bot | no — you never see the seeds |
| What it measures | how good this bot is on seeds it could tune against | whether that generalizes |
Public is where you work. The seeds come from a published secret, so
nethackers eval on your laptop produces the same batch as everyone else's and
you can iterate against it freely. This is the intended target, not a consolation
prize: NetHack is unsolved by a wide margin, and the starting bet is that plainly
getting better at the public dungeons is where the first generalizable gains come
from.
Private answers the other question — does it transfer? A trusted verifier re-runs registered programs on seeds held secret by the hub and submits an independent score, so nobody has to take a self-reported number on faith. The website defaults to Private because it is the harder question; it is not a verdict on how you got there.
The two words are deliberately kept apart. Verified is the trust level (a
trusted worker produced this number). Hidden is the seeds it happened to use.
Full mechanism, including the verifier's scheduler:
docs/verification.md.
Reference architecture
linux/amd64 is the scoring architecture — every score the hub accepts came
from the pinned amd64 arena image, which is what nethackers eval uses by
default. That's what makes "the same batch as everyone else's" (above) true
regardless of what you're running it on.
Other hosts emulate, and that's required, not optional: the same seed plays a different game of NetHack on a different CPU architecture, so a native arm64 score isn't comparable — the hub refuses it.
On Apple Silicon, turn on Rosetta in Docker Desktop (Settings → General →
Apple Virtualization framework → "Use Rosetta for x86_64/amd64 emulation").
It's worth doing: the same 15-episode batch on the same machine took 823s
under QEMU and 224s with Rosetta. nethackers doctor reports whether it's on.
Safety: this runs untrusted code
⚠️ Read this before running
evolve,eval, orpull.
Three things execute code that neither you nor we wrote or reviewed:
- Bots you evaluate.
nethackers evaland every eval insideevolveimport and run abot.py. If you pulled it from the hub, someone else wrote it. - The coding agent.
nethackers evolveruns Claude Code, Codex, or OpenCode with permission prompts fully disabled (--dangerously-skip-permissions/--dangerously-bypass-approvals-and-sandbox). It writes and executes code unattended, for hours. - Programs you
pull.nethackers pullclones a repo. Nothing runs at clone time, but you now have a stranger's code on disk that the nextevalwill execute.
What we do about it:
- The bot evaluator is a sealed box. Every eval runs in a container with no
network (
--network none), a read-only root filesystem, anoexec,nosuidtmpfs for scratch, every Linux capability dropped,no-new-privileges, a non-root user, and pid/memory/CPU caps. The bot is mounted read-only, and the hidden seeds never enter the container: the secret is expanded to concrete per-game seeds on the host and piped in over stdin, so it is never on the container's argv or in its environment. - Fetching is github-only and hardened.
pulland hub registration accept onlygithub.com/<owner>/<repo>@<commit>references. The host is parsed rather than string-matched, so lookalikes likegithub.com.evil.comorgit@github.com:...are refused, and the clone allows only https, with submodules, symlink checkout, and tags disabled. - The coding agent runs in a container with
no-new-privileges, pid/memory (swap-capped)/CPU limits, a non-root user (it starts as root only to remap uids, then drops), and a wall-clocktimeout. Instruction-bearing files (CLAUDE.md,AGENTS.md,.mcp.json, …) are stripped from the tree it is handed, and an opt-in credential broker can keep your model key out of the container entirely.
What we don't do:
- A bot can still influence its own score. The scorer runs the bot in-process
with the solution on its
sys.path, so a self-reported number is a claim you take on trust. Sealing the container does not change that. It is why the Private Dungeons (verified) tier exists. - The credential broker is opt-in. By default the agent's container still has your coding-agent credentials mounted (for Codex, writable) and open network egress. The agent CLIs need their model APIs, and egress allow-listing is designed but not on by default.
- The threat model is accident-grade. It defends against a runaway or confused agent and the blast radius of one, not a determined adversary. A container is not a boundary against a kernel exploit. If you are evaluating code you have reason to distrust, run it on a machine you are willing to lose.
Details, per-surface, are in docs/harness.md,
written to be reused by anyone building a harness of their own.
Install
Requirements
Every row also needs Python 3.11+; the rest are additive per row, not cumulative down the table.
| To do this | You need |
|---|---|
| Browse the hub (TUI, boards, frontier) | nothing else |
eval — score a bot |
Docker or Podman |
evolve — run the loop |
Docker or Podman, a coding agent CLI (claude, codex, or opencode2) logged in on the host, and nethackers login |
submit — publish a solution |
Docker or Podman (it evaluates before pushing), nethackers login, and gh authenticated as the same GitHub account |
OpenCode 2 needs nothing installed on the host: its CLI ships in the sandbox.
Give it models by defining providers in your global
~/.config/opencode/opencode.json (or .jsonc). Only that file's provider
section enters the sandbox, along with the environment variables those
providers reference as {env:NAME} or list in their env field. So set each
key as {env:NAME} or a literal apiKey: a {file:...} key, a login made with
opencode2 auth login, your plugins and MCP servers, and project
opencode.json files all stay outside. With no key, runs use OpenCode's free
models, and doctor says so. How each coding agent behaves in the sandbox, with
OpenCode 2 in detail, is in docs/harness.md.
Note that doctor's publish capability checks the hub, your login, and gh —
but not the container runtime, so it can report publish ready on a machine where
submit will still stop at its arena evaluation.
Install the CLI
pip install nethackers
# or, isolated:
uv tool install nethackers
Don't have uv? It installs from https://astral.sh/uv.
Check your machine
nethackers doctor
doctor runs eight checks and folds them into four capabilities — browse,
eval, evolve, publish — telling you exactly which ones this machine can do
and what to fix for the rest. It honors -o json if you want to gate a script on
it. It reaches the network (a hub round-trip, and a registry probe for any
sandbox image you don't have locally), and it changes nothing unless you pass
--pull.
The sandbox images (ghcr.io/dunnolab/nethackers-arena and -mutator) are
pinned by digest in the CLI and pulled on first use — several GB, so the first
eval takes a while. Nothing else needs building.
Quickstart
Browse
nethackers # the dashboard TUI: frontier, boards, elites, your runs
Every view is also a plain command, and each prints JSON when piped:
nethackers frontier # how far the community has collectively reached
nethackers board --scope val # ranked programs; scope = generalist | role |
# facet (race:elf) | full identity
nethackers elites --scope val-dwa-law-fem # best program per identity
nethackers search --owner <login> # registered programs, filtered
nethackers show <prog_id> # one program in detail
nethackers elites -o json | jq '.[0]'
Score a bot
nethackers eval ./my-bot --objective val-dwa-law-fem
Runs the identity's published 15-seed batch in the sandboxed arena and prints the result. This is the Public Dungeons number.
Evolve one
nethackers login # GitHub device flow, once
nethackers evolve val-dwa-law-fem \
--seed autoascend \
--operator codex \
--iterations 20
The objective is positional; --seed points at the starting solution. AutoAscend
ships inside nethackers, so autoascend works from anywhere, installed or from a
checkout. The older spelling roots/autoascend still resolves to the same tree. Each iteration picks a cell, mutates its elite
with the coding agent in its container, and evaluates the result.
Every candidate that survives the smoke check and gets evaluated is then pushed
to your public nethacker repo and registered — not only the ones that improve
on their parent. That is deliberate (the archive is meant to record what was
tried, not just what won), but it means a long run publishes a lot of commits
under your account. Use --offline to run the loop without publishing or
registering anything.
The objective resolves to a set of identities: a single one (val-dwa-law-fem),
a bare role (val — all its identities), a comma list, or a glob
(val-*-law-*).
Publish an existing solution
nethackers submit ./my-solution --objective val-dwa-law-fem
Evaluates it, pushes it to github.com/<you>/nethacker (on its submit
branch), and registers the resulting repo@commit with the hub. Requires gh
authenticated as the same account you nethackers login'd with.
Fetch anyone's program
nethackers pull github.com/<someone>/nethacker@<commit> ./fetched
Anything registered from a public repo is fetchable by anyone, pinned to the exact commit that was measured.
The hub defaults to https://nethackers.dunnolab.ai; override with --hub or
$NETHACKERS_HUB. nethackers --help lists every command.
GitHub is the infrastructure
We deliberately do not run an artifact store, an identity provider, or a code host. GitHub is all three, which keeps the hub small enough to be honest about.
- A program is a
repo@commit. The hub stores the link, the manifest, and the scores — never the code. Registration is rejected unless the reference is a realgithub.com/<owner>/<repo>(the host is parsed, so lookalikes are refused) and the commit exists (the hub checks it against the GitHub API using your token), so a board row always pointed at a real tree, on GitHub, when it was made. Two honest limits: the hub does not check repo visibility, so a private repo can be registered and will not be fetchable by others —submitforces the repo public, a hand-rolledregisterdoes not — and a link is only as durable as the repo behind it, which its owner can force-push, delete, or flip private. - Identity is your GitHub account.
nethackers loginis a GitHub App device flow. There is no client secret and no App private key anywhere in this repo or on the hub — the hub reads GitHub with the caller's own token. Your board identity is your GitHub login. - Publishing is a push.
nethackers submitusesghto create/pushgithub.com/<you>/nethackerunder your own account. You own your solutions; we hold a pointer. Bots go to branches (submit, and one perevolverun); on a newly created repo the default branch holds only a README. While the repo's description is still empty, a publish fills it in, links the repo to your page here, tags itnethack/nethackers, and adds a short README if there is none — it never overwrites a description, website or README you wrote, and all of it is yours to edit or delete. - CI is the deploy lever. Pushing a
vX.Y.Ztag builds the hub image, pushes it to GHCR, and flips production by digest with a health check and automatic rollback (skippable with[skip hub-deploy]in the tagged commit message). The sandbox images are built by workflow and pinned by digest into the CLI. Seedeploy/README.md.
Documentation
| Doc | What's in it |
|---|---|
docs/harness.md |
The ArenaBot contract, how our evolutionary harness works, safety and sandboxing, and how to build your own harness |
docs/verification.md |
The verified tier: hidden seeds, the verifier's scheduler, the AutoAscend floor, what's public |
docs/contributing.md |
Dev setup, tests, conventions, PR flow |
docs/troubleshooting.md |
Symptom → cause → fix |
docs/local-stack.md |
Running a full local hub + arena + mutator stack |
deploy/README.md |
Operating the production hub |
Contributing
Bug reports, bots, and harnesses are all welcome — and you do not need to be
good at NetHack for any of them. Start with
docs/contributing.md.
License
Apache-2.0. See LICENSE. AutoAscend, vendored at
src/nethackers/roots/autoascend/ and so shipped inside the wheel, carries its
own MIT license — see
LICENSE, which travels with the
tree wherever it goes.
Metadata
Release files for nethackers 0.34.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nethackers-0.34.4.tar.gz | 1.0 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nethackers-0.34.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.6 MB
Release files / nethackers-0.34.4.tar.gz
| Download URL | nethackers-0.34.4.tar.gz |
|---|---|
| Size | 1.0 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e4b72527492f33e8d1e06d432fd1e50443c01e1411b090233a05b62f3e4f6c7c
|
|
BLAKE2b-256 checksum How to use checksums |
83f54a3d0f86dcfae968eb9942d836efc04d69810fa51a6858990bca7e3ab8df
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / nethackers-0.34.4-py3-none-any.whl
| Download URL | nethackers-0.34.4-py3-none-any.whl |
|---|---|
| Size | 583.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
46f73a0086da936176c60df2c55fb92305baf8510aa652ff77ca2a2c3d454b11
|
|
BLAKE2b-256 checksum How to use checksums |
a4a602a4b71371f1b4cdc017f08e80881738f8658f74da99ba9bc7188642bd67
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|