CLI reference¶
Every WhyGraph command and its flags. Run whygraph <command> --help to see the same text from your
own install. There are three commands in the image, plus the host verbs the Docker shim adds and the
stubs that stand in for removed ones.
$ whygraph --help
Commands:
install Emit the host shim installer (called by scripts/install.sh).
portal Run the WhyGraph portal (use `whygraph up` to start it in Docker).
version Print installed whygraph version.
Host commands¶
On the Docker install the whygraph shim handles these on the host, before any container of its own
runs. They manage the portal's two long-lived containers, whygraph-portal and its database,
whygraph-portal-postgres; they are not Python subcommands. The full walkthrough is
Start the portal.
| Command | What it does |
|---|---|
whygraph up [--port N] [--add-folder DIR] |
Start the database container, wait up to 60 seconds for it to be healthy, then start the portal container, or recreate the portal when the shared folders, port or image changed. The database is recreated only when it is missing, stopped, or on a different image than the pin, and a running one is dumped first (see backup). --port is remembered in ~/.config/whygraph/port; --add-folder (repeatable) shares a folder. No-op when both are already running unchanged. |
whygraph down |
Stop and remove both containers, the portal first, waiting up to 30 seconds each (WHYGRAPH_STOP_GRACE overrides) so a running scan is recorded as interrupted. The database's files stay in the data directory. |
whygraph status |
Each container's state - running, restarting or not created - plus the URL, image, database image and shared folders. Exits non-zero unless both are running. |
whygraph logs |
Follow the portal container's logs. For the database: docker logs whygraph-portal-postgres. |
whygraph folders [--remove DIR] |
List the shared folders, or remove one and recreate the portal container. |
whygraph backup |
Dump the running portal database (pg_dump -Fc, run inside the database container) to backups/portal-<UTC time>.dump in the data directory, keeping the newest 10 such dumps; other files there are never touched. Fails if the database is not running. Restore needs the data directory's secret.key too. See Backup and restore. |
whygraph update [--version X.Y.Z] [--check] [--force] |
Update to the newest release, or to --version: resolve it, download that release's installer, dump the database, install, and recreate the portal with up. --check prints the current and latest versions and changes nothing. A release (or one in between) that needs manual steps stops with exit code 2 until --force. Downgrades are refused. See Upgrading. |
Any other argument to these verbs is rejected with exit code 2, as is every refusal: a database
that does not become ready, a failed pre-recreate dump (WHYGRAPH_SKIP_BACKUP=1 skips it), a
database created by another Postgres major version (see
the recipe), or a missing postgres.password when the
database already exists. update also refuses a downgrade, a release that needs manual steps
(without --force), a dev portal (WHYGRAPH_PORTAL_NAME or WHYGRAPH_DEV_SRC set), a
WHYGRAPH_IMAGE override and a host without curl. Port precedence for up is the remembered --port, then WHYGRAPH_PORT,
then 8765. up never passes your environment into the
container, and on the first run that sees a credential variable set it prints its name once, as a
reminder to enter the key under Settings.
whygraph version¶
Print the installed package version. No options.
whygraph portal¶
Run the multi-project portal: one long-running server that holds many projects, with the
Explorer and Chat per project and a per-project HTTP MCP endpoint at /mcp/<slug>. It is the
command the Docker runtime runs inside the image.
Development only when run natively
In this release the supported way to run the portal is the Docker runtime, which publishes it on
127.0.0.1 only. Run natively, whygraph portal is for development: shared folders come only
from WHYGRAPH_SHARED_FOLDERS, and a non-loopback --host is refused unless --dev-expose is
given, because local mode has no login. The compose bundle for
production mode runs the portal in the image with --host 0.0.0.0,
behind a TLS proxy.
| Option | Default | Description |
|---|---|---|
--host |
127.0.0.1 |
Bind address. Outside the image, anything but a loopback address needs --dev-expose. |
--port |
$WHYGRAPH_PORT or 8765 |
Port to bind. It is also the port the browser and agents use, so the allowed Host / Origin values and the agent MCP URLs are built from it. |
--data DIR |
$WHYGRAPH_DATA or ~/.local/share/whygraph |
Portal data directory: the encryption key, scan run files, cloned repositories, each project's history (projects/). Keep it outside every project folder. |
--dev-expose |
off | Allow a non-loopback --host outside the image. |
The portal keeps its own data in Postgres, named by WHYGRAPH_DATABASE_URL (plus an optional
WHYGRAPH_DATABASE_PASSWORD_FILE; see Configuration). The
shim sets both for its database container. On start the portal waits up to a minute for the
database, and migrates it.
| Exit code | When |
|---|---|
2 |
WHYGRAPH_DATABASE_URL is unset or not a Postgres URL, or the password file cannot be read; another portal already holds the data directory; a refused --host; a refused production start (an invalid WHYGRAPH_BASE_URL or WHYGRAPH_TRUSTED_PROXIES, shared folders set, or a missing or invalid GitHub sign-in setting: no WHYGRAPH_GITHUB_OAUTH_CLIENT_ID, an unreadable or empty WHYGRAPH_GITHUB_OAUTH_CLIENT_SECRET_FILE, or a bad WHYGRAPH_GITHUB_URL / WHYGRAPH_GITHUB_API_URL), with the reason on stderr |
3 |
The database stayed unreachable through the start-up wait. Under the shim, Docker's restart policy retries with back-off. |
The portal is a single process. It holds an exclusive lock on its data directory, so a second
whygraph portal on the same directory exits with code 2, and a Postgres advisory lock on its
database for as long as it runs, so a second portal on the same database (with another data
directory) starts degraded with another WhyGraph portal is already using this database and
touches nothing. If the database connection that holds that lock drops, the portal shuts down so the
restart policy can bring it back and take the lock again. An advisory lock is held by a database
session, so an external database must be reached directly or through a session-mode pooler;
a transaction-mode pooler such as PgBouncer's makes the lock meaningless. In local mode every /api request must carry the
X-WhyGraph-Client: 1 header and a loopback Host; cross-site requests and foreign Origin values
are rejected. WHYGRAPH_DEV_ORIGINS (comma-separated origins, e.g. http://localhost:5173) adds
origins for a local frontend dev server.
More variables select and shape production mode - WHYGRAPH_MODE,
WHYGRAPH_BASE_URL and WHYGRAPH_TRUSTED_PROXIES, the two required GitHub sign-in ones,
WHYGRAPH_GITHUB_OAUTH_CLIENT_ID and WHYGRAPH_GITHUB_OAUTH_CLIENT_SECRET_FILE, and the five
required GitHub App ones, WHYGRAPH_GITHUB_APP_SLUG, WHYGRAPH_GITHUB_APP_CLIENT_ID,
WHYGRAPH_GITHUB_APP_CLIENT_SECRET_FILE, WHYGRAPH_GITHUB_APP_PRIVATE_KEY_FILE and
WHYGRAPH_GITHUB_APP_WEBHOOK_SECRET_FILE (two optional ones point at GitHub Enterprise Server); see
Configuration. In production the Host is the base host or
an organization's subdomain, and the guard described above is replaced by the one on the
security model page.
The portal never follows a symbolic link out of a project's folder, since a repository's content
(especially a production portal's copy of a GitHub repository) is not trusted. When .whygraph/, .codegraph/, the CodeGraph index,
.gitignore, whygraph.toml, a portal marker, an agent config file or a bundled agent folder
(such as .claude/) is a symlink, the portal refuses it with an unsafe_path error: Initialize,
the Explorer, Chat, the MCP endpoint and scans all stop for that project, a whygraph.toml link is
not imported, and removing the project leaves the linked files alone. Replace the link with a real
file or folder to continue.
Scans in the portal¶
The portal runs every scan itself, as an internal scan child process in the project's folder. Each project scans one run at a time; a request that arrives while a run is going joins the single queued run instead of starting another, so a burst of commits costs one follow-up scan. At most two projects scan at once.
| Trigger | Started by | LLM descriptions |
|---|---|---|
initial |
Any request while the project has no successful scan yet | No (structure only) |
hook |
A git hook after a commit, merge, rebase or checkout, and the catch-up check | No, and offline (a Quick rescan) |
push, reconcile |
Production mode: a push to the default branch (the GitHub App's webhook), and the hourly check of the default branch on GitHub | No |
forge |
Production mode: GitHub activity on pull requests, issues, milestones or tags. It refreshes only those and the tags, never the commits or the code index | No |
manual |
Scan now | Yes (unless turned off for that run) |
describe |
Describe core or Describe all (the project's describe buttons). Core is the commits behind the most-referenced files; all is every commit. Describe core with Also describe large commits file by file unticked leaves those file changes out. Merged requests run as the widest one: a queued core run that a Describe all or a Full rescan joins runs as all | Yes |
The first scan never calls the LLM. Afterwards the project shows how many commits a full scan would describe, with which model and a token (and, for known models, cost) estimate, so you choose when to spend. In local mode, when the portal starts and every 15 minutes, it also rescans a local project whose checkout moved past the last scanned commit while the portal was not running.
In production mode every run of a project first syncs its server copy: it fetches the default
branch with a repository-scoped installation token and checks it out, then scans. A push or
reconcile run scans only when the branch moved past the last scanned commit; Scan now, initial and describe
always scan. A forge run always refreshes the GitHub data, at most once every 5 minutes per project
(later activity waits and joins one queued run), and never counts as a scan of the branch, so the
next push or reconcile run still scans the commits it fetched. The scan gets the token through a 0600 file named by WHYGRAPH_GITHUB_TOKEN_FILE,
rewritten before it expires, never through its environment. hook scans exist only in local mode.
Only an allowlist of the portal's environment reaches a scan (PATH, HOME, locale, TZ, the TLS
bundle and proxy variables); API keys and GitHub tokens come only from the portal's own settings (in
production, from the GitHub App), and never appear in run files: each run's progress (runs/<id>.jsonl) and log (runs/<id>.log)
under the data directory show a key as its last four characters. Stopping the portal stops running
scans and records them as interrupted.
The portal's HTTP API exposes the same run data the web UI shows:
| Endpoint | Returns |
|---|---|
GET /api/projects/<slug>/scans |
The project's runs, newest first, 50 per page: {runs, next}. Filters: status and trigger (comma lists), type (full, quick or sync), requester (a user id or system); limit (1-100) and before=<next> page through |
GET /api/projects/<slug>/scans/<id> |
One run: its trigger, status, times, who requested or cancelled it, and its summary. Cost and the estimate are shown to project admins only |
GET /api/projects/<slug>/scans/<id>/events |
The run's progress as a server-sent event stream (resumable with Last-Event-ID) |
GET /api/projects/<slug>/scans/<id>/log |
The end of the run's log, at most the last 64 KiB, starting at a line boundary: {run_id, text, size, truncated}. Keys are already masked. |
GET /api/portal/state |
Portal status, including the installed version (what whygraph version prints) |
Every /api call needs the X-WhyGraph-Client: 1 header, and starting a scan
(POST /api/projects/<slug>/scans) answers 409 until the project is initialized.
JSON progress¶
The scan child is an internal program, python -m whygraph.scan.child; nobody types it, and
whygraph scan no longer exists. It always reports progress as JSON lines on stdout, which is how the
portal's runner shows a run live. Stdout carries one JSON object per line, each written whole, so a
reader can parse line by line. Logs and errors stay on stderr. The type key says what a line is:
type |
When | Fields |
|---|---|---|
start |
Always first | phase_total - the number of phases this run executes (1 to 5, depending on the run's flags and whether an LLM is configured); phases - their titles in order (["Code index"] for a code-index-only run), so a reader can name every phase before it begins. Optional: events recorded by older runs lack it |
phase |
Each phase begins | phase (1-based), title |
task |
A crawler registers, and as it progresses (throttled; a finished task is always sent) | name (stable crawler label), completed, total (null while unknown), description (current status text) |
result |
Always last | status (ok or failed), elapsed_sec, phase_timings, crawlers (per crawler: name, status, summary, and error / warning when set), analyze_skipped (why the LLM phase was skipped, else null) |
The exit code is 1 when any crawler failed.
Removed commands¶
The portal replaced these entry points, and an old habit or a stale script gets a message rather than "no such command".
| Removed | Was | Now |
|---|---|---|
whygraph init |
Bootstrap the database, write config, wire an agent | Add the repo on the portal's Projects page; it initializes it |
whygraph serve |
The single-repo web panel | The portal, for all your projects |
whygraph-mcp |
The stdio MCP server | The portal's HTTP endpoint at /mcp/<slug> |
whygraph scan |
Crawl the repo and describe commits, in a terminal or a CI job | The portal runs every scan: Scan now (Full rescan or Quick rescan) on the project's page, or the git hooks |
whygraph analyze |
Describe one commit and print it | Gone; descriptions come from a Full rescan, Describe core and Describe all |
whygraph init, whygraph serve, whygraph scan and whygraph-mcp still exist as stubs: each prints a pointer to
whygraph up on stderr and exits with status 2. The whygraph-mcp stub writes nothing to
stdout, so an agent that still launches it over stdio reads nothing it could misparse, and it is a
plain script that never starts Docker. whygraph serve --stop is the one remaining use of the old
verb: it removes a leftover 1.x whygraph-serve container. whygraph analyze has no stub.
See Upgrading from 1.x.
whygraph install¶
Plumbing, not a step you run
This command exists inside the image so the installer can call it. Installing WhyGraph is
the curl … | sh line in Installation - you don't run
whygraph install yourself.
Prints the POSIX sh script that writes the whygraph shim and the whygraph-mcp removal stub onto
your PATH, pinned to the image's baked version. scripts/install.sh runs it via
docker run --rm IMAGE whygraph install and executes the output; keeping the shim bodies here (in
tested Python) rather than in the fetched shell is what stops the two from drifting.
No options. Two reasons you might still invoke it directly:
| Command | Why |
|---|---|
docker run --rm ghcr.io/mtrdesign/whygraph:2.5.0 whygraph install |
Read exactly what would be written to your PATH, without writing it. |
docker run --rm ghcr.io/mtrdesign/whygraph:2.5.0 whygraph install \| sh |
Install with no curl - air-gapped hosts, CI images. |