Installation¶
Winnow is a Python program you run on your own machine: one server process, a browser window pointed at it, and no network needed once it is installed. This page covers getting it, what it needs, starting it, every command-line option, where it keeps its files, and how to update, roll back and remove it.
Requirements¶
- Python 3.10 or newer. Python 3.9 and older stop at startup.
- Four packages and their dependencies, from
requirements.txt: FastAPI, uvicorn (with its[standard]extras), python-multipart and openpyxl. Install from the file rather than by hand: openpyxl is imported at startup, and without it the server stops before it binds. - A browser. A Chromium-family browser (Edge, Chrome, Brave, Chromium) gets you an app window; any other browser works as an ordinary tab.
- Windows, macOS or Linux. All three have double-click launchers. File associations are Windows and Linux only.
Nothing is fetched at runtime: no CDN, no web fonts, no update check unless you press the button. That is what makes it usable on an airgapped analysis box.
Getting Winnow¶
From a release¶
Releases are published on GitHub at
https://github.com/jacsonott/Winnow/releases, tagged v0.1.0 through
v0.4.0 so far. Download Source code (zip) from the latest one and unzip
it wherever you want Winnow to live. That folder is the install: Winnow keeps
its settings, plugins and (by default) your cases inside it, which is why
updating is done in place rather than by replacing the folder (see
Updating).
From git¶
git clone https://github.com/jacsonott/Winnow.git
The default branch, main, holds released versions only. Development
happens on develop; check it out if you want unreleased code.
Update a clone with git pull like any other repository. A clone reports
the version written in winnow/version.py, and on develop that is still
the last release's number, so give the commit as well when you state which
build you used.
Installing the Python packages¶
A virtual environment keeps Winnow's packages away from the system Python, and on recent Linux distributions pip refuses to install into the system Python at all ("externally managed environment"). From the Winnow folder:
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
Name it .venv or venv and keep it inside the Winnow folder: updates never
touch either name.
The double-click launchers do not look for a virtual environment. They run
the first python3 or python on your PATH (on Windows, python and then
py -3). If the packages live only in a venv, start Winnow with that
environment's interpreter instead: .venv/bin/python server.py, or
.venv\Scripts\python server.py on Windows.
On an airgapped machine¶
Winnow itself needs nothing from the network; only the packages have to be carried in. One way, using pip's own tools, is to collect the wheels on a connected machine with the same operating system, CPU architecture and Python version as the target (some of the dependencies are compiled, so the wheels are platform-specific):
python -m pip download -r requirements.txt -d wheels
Carry the wheels folder over with the Winnow zip, then on the target:
python -m pip install --no-index --find-links wheels -r requirements.txt
First launch¶
From the Winnow folder:
python server.py # python3 on most Linux and macOS installs
The terminal prints Winnow on http://127.0.0.1:8777 (home screen — no case
open) and a window opens on the home screen. Winnow plays a short launch
animation (any key or click skips it, and Settings → Appearance turns it
off) and asks two questions, once each:
- whether you are working over RDP or another remote desktop. Enable remote mode switches the grid to whole-row scrolling and stops animations; you can change it later in Settings → Remote session.
- where case files should live. Use the default puts them in the
casesfolder inside the Winnow folder (the prompt shows its full path); Choose a folder… lets you pick one.
Start Winnow from its own folder, as the launchers do: a case created by
--open without --case goes in the folder you started in. The default
cases folder is always the one inside the Winnow folder.
From the home screen you create or open a case; see Cases and importing.
App window or browser tab¶
Winnow looks for a Chromium-family browser and opens itself in that browser's app mode: a window with no address bar or tabs. It waits for the server to answer (up to 15 seconds) before opening anything. The first browser found wins:
| OS | Looked for, in order |
|---|---|
| Windows | Microsoft Edge, Google Chrome, Brave, Chromium (in Program Files, Program Files (x86), and for Chrome and Chromium also your local AppData) |
| macOS | Google Chrome, Microsoft Edge, Brave Browser, Chromium (in /Applications, then ~/Applications) |
| Linux | the first of google-chrome, google-chrome-stable, chromium, chromium-browser, microsoft-edge, microsoft-edge-stable, brave-browser on your PATH |
If none is found, or the browser fails to start, Winnow opens an ordinary tab in your default browser instead. Firefox has no app mode, so it is only ever used that way.
The app window runs in your normal browser profile, so your extensions run in
it too, and Winnow's per-browser preferences (keyboard bindings, among
others) are stored in that profile. --browser-tab opens an ordinary tab
instead, and --no-browser opens nothing, leaving you to browse to the
printed address. --browser-profile DIR gives the app window a profile of
its own in DIR: your extensions stay out and Winnow gets a separate taskbar
entry, but keyboard bindings and the other per-browser preferences start from
their defaults there. It has no effect with --browser-tab.
Closing the window is enough to stop Winnow: the server exits on its own shortly afterwards (see Idle shutdown).
Double-click launchers¶
launch/ holds launchers for people who would rather not open a terminal.
Each finds Python and starts server.py from the folder above it, so they
need Python and the packages installed exactly as above. Full details are in
launch/README.md.
| OS | File | Notes |
|---|---|---|
| Linux | winnow.sh |
Mark it executable, or allow launching, if your file manager asks. |
| macOS | winnow.command |
Finder runs it in Terminal. The first run may need right-click → Open (Gatekeeper). |
| Windows | winnow.bat |
Opens a small console window that stays while Winnow runs. |
| Windows | winnow.vbs |
The same, with no console window. |
Leave the launchers where they are: each one works out the install folder
from its own location, so a copy on the Desktop finds nothing. Make a
shortcut, alias or desktop entry that points back at the original instead.
The shell and batch launchers pass any arguments you give them on to
server.py.
On Linux, if you make a desktop entry as launch/README.md describes and
also use file associations,
give your entry a name other than winnow.desktop. Winnow writes its own
~/.local/share/applications/winnow.desktop when you register a file type,
and deletes it when you unregister the last one.
Command-line options¶
python server.py --help lists these too.
| Option | Default | What it does |
|---|---|---|
--case FILE |
none (home screen) | Open this case file, creating it if it does not exist. The case is added to the home screen's list. |
--open FILE … |
Import these delimited text files (CSV, TSV and the like) into the case at startup. Without --case, the case is case.db-winnow in the current folder. |
|
--no-fts |
off | Skip the full-text index for the --open imports. Import is faster; search falls back to scanning. |
--port PORT |
8777, or the next free port |
Port to listen on. Without it, a Winnow started while another has 8777 takes the next free port and opens its own window there. With it, Winnow uses that port or stops. |
--host HOST |
127.0.0.1 |
Address to bind. See Reaching Winnow from another machine. |
--allow-host NAME |
Accept this name in the Host header as well. Repeatable. | |
--force |
off | Open --case even if another Winnow has it open. |
--no-idle-shutdown |
off | Keep running with no window connected, for driving Winnow from scripts. |
--no-browser |
off | Open no window. |
--browser-tab |
off | Open an ordinary browser tab instead of an app window. |
--browser-profile DIR |
Run the app window in its own browser profile. | |
--plugins-dir DIR |
Load plugins from this folder as well. Repeatable. plugins/ and $WINNOW_PLUGINS_DIR are always scanned. See Plugins. |
|
--assoc FILE … |
What file associations run on a double-click. You do not normally type it. |
To jump straight into a case with data in it:
python server.py --case acme.db-winnow --open EvtxECmd_Output.csv
Each --open file prints its row count and import speed as it goes, plus a
note if rows had the wrong number of columns or the file was not UTF-8. For
workbooks, SQLite databases, JSON and the other formats, import from the UI.
--case refuses a file that exists but is not a Winnow case, rather than
writing Winnow's tables into it. To work with a SQLite database as evidence,
open a case and import the database.
One case, one Winnow¶
A case file should be open in one Winnow at a time. Each open case leaves a
<case>.winnow-lock marker beside it, and a second Winnow that tries to open
the same case refuses on the command line, naming who has it:
ERROR: /cases/acme.db-winnow is already open in another Winnow.
analyst@lab-01 (pid 4650), open since 2026-10-13T05:36:46, last seen 21s ago
...
Pass --force to open it anyway.
From the home screen you get the same information and a choice of Open anyway or Don’t open. If the other Winnow was killed rather than shut down, its marker can still read as live for up to two and a half minutes; after that the case opens normally. Keep case files off network shares: SQLite's locking does not work over SMB or NFS. Two analysts on one investigation work in separate case files and exchange session files (see Sessions and export).
Several Winnows can run at once, each on its own port and its own case. If
the port is already taken, the second one stops at startup with an "address
already in use" error; pick another with --port.
Reaching Winnow from another machine¶
By default Winnow listens on 127.0.0.1 only. --host 0.0.0.0 (or a
specific address) makes it reachable from the network, and it prints a
warning when you do: Winnow has no login and no access control, so anyone who
can reach the address can open, import into or query any case the server can
reach. Only do this on a trusted, isolated network.
Winnow also checks the Host header of every request. It answers to
localhost, 127.0.0.1, ::1, any IP address, the --host value, and any
name given with --allow-host. Anything else, including the machine's own
hostname, gets 421 Unrecognised Host header. So a colleague can browse to
http://10.0.0.5:8777, but to use http://lab-01:8777 start Winnow with
--allow-host lab-01. The same applies to a reverse proxy's name.
Some things only work from the machine Winnow runs on: file associations, the Environment settings, creating folders, the import dialog's Add from this machine… picker, quick-look cases and installing plugins.
If you drive Winnow's API from a script, every request that changes
something (anything but GET) must carry the header
X-Timeline-Lite-Client: 1, or it is refused with 403. That rule is what
stops a web page in another tab from acting on your case.
Idle shutdown¶
A Winnow with no windows left shuts itself down, so a forgotten server does not hold a case open all weekend. Every Winnow window keeps a connection open to the server, and the server watches the count:
| Situation | Winnow exits after |
|---|---|
| The last window was closed | about 2 minutes |
| Windows stopped answering without closing (laptop asleep, browser suspended the tab, VM paused) | 30 minutes |
No window ever connected (for example --no-browser and nobody browsed to it) |
15 minutes |
It never exits while a window is connected, while a request is still running (a long export keeps downloading after its tab closes), or while background work is going on: an import, a Save as table copy, a watchlist scan, or a search still building. A reload is covered by the grace period. When it does exit it prints the reason in the terminal, and any window still open shows "Lost the connection to Winnow’s server" with a Try again button.
--no-idle-shutdown turns it off.
Stopping Winnow¶
Any of these stops the server cleanly. Everything already committed (tags, notes, finished imports) is in the case file.
- ⏻ Shut down on the home screen.
- Settings → Shut down → ⏻ Shut down Winnow.
- Case ▾ → Shut down Winnow… inside a case.
Ctrl+Cin the terminal it was started from.- Closing every window, then waiting (see above).
The buttons ask first, and warn if an import or other work is still running or if other Winnow windows are open on the same server. The server then stops straight away, with its windows still open: the window you pressed it in says "Winnow is off", and any other shows "Winnow’s server has shut down" with a Try again button. A download already in progress, such as a long export, is allowed to finish first.
Where things live on disk¶
Inside the Winnow folder:
| Path | What it holds |
|---|---|
workspace/ |
Cross-case state as readable JSON: the home screen's case list, saved filters, the default tag template, column layouts and header nicknames, timeline templates, import profiles, which plugins are on, profiles, the dashboard library, app settings, the cases folder setting, plugin data, and the list of running Winnows. Deleting it loses your case list and these preferences, never analysis. |
cases/ |
The default home for case files, unless you chose another folder at first run. |
cases/quicklook/ |
Temporary cases created when you double-click a file through a file association. One you never saved and never did any work in is deleted after seven days. |
plugins/ |
Plugins you installed. The bundled examples live in examples/plugins/. |
sessions/ |
A folder older versions created beside a case file for saved sessions. Current versions keep sessions inside the case file; updates leave any sessions/ folder alone. |
.winnow-backup/, .winnow-install.json |
The updater's backups of previous versions and its record of what it installed. |
A case is one file, name.db-winnow, and it holds everything you did in it:
tags, notes, saved sessions, layouts, derived columns, dashboards. While it
is open you will also see name.db-winnow-wal, name.db-winnow-shm and
name.db-winnow.winnow-lock beside it. Older cases ending in .db still
open. The evidence files you import are read, never modified.
Outside the Winnow folder:
| What | Where |
|---|---|
| Keyboard bindings, striped rows, the autofit limit, the filter-row choice, panel sizes | The browser's storage for Winnow's address, per browser profile. |
| Skin, theme, accent, row density, launch animation, Pages as a dropdown, Open new tables when an import finishes | The browser's storage, plus a machine copy in workspace/ that a new browser, profile or port starts from. |
| Environment variables set in Settings → Environment | Windows: your user environment (HKCU\Environment). Elsewhere: ~/.config/winnow/env. |
| File associations | Windows: HKCU\Software\Classes. Linux: ~/.local/share/applications/winnow.desktop, ~/.config/mimeapps.list, and for plugin and case types ~/.local/share/mime/packages/winnow.xml and icons under ~/.local/share/icons/hicolor/. |
| Scratch file for the open case's views | /dev/shm on Linux, otherwise the system temp folder, named winnow-views-*. Deleted on exit; leftovers from a crash are cleaned up at the next start. |
Updating¶
An update replaces the files Winnow ships and nothing else, after backing up the ones it replaces. Winnow never checks for updates on its own; every check is something you clicked or typed.
From Settings¶
Settings → Updates shows the installed version. Check for updates
asks GitHub for the latest release. If there is a newer one it shows the
release notes and an Install update button, which asks for confirmation,
downloads and installs it. Restart Winnow afterwards: the running server
keeps running the old code until it restarts. Settings has no rollback
button; use update.py --rollback for that.
From the command line¶
update.py does the same from a terminal and needs no running server, so it
still works when a bad update has broken the UI. Shut Winnow down first.
python update.py --check # what's available; changes nothing
python update.py # check, show the plan, ask, apply
python update.py --dry-run # show the plan and stop
python update.py --rollback # undo the last update
| Option | What it does |
|---|---|
--check |
Report the installed and latest versions, and the release notes if newer. |
--dry-run |
Show the plan, then stop: how many files would be changed, added and removed (the first few named), and which folders are preserved. |
--from ZIP |
Apply a release archive already on this machine instead of downloading one. |
--download-only |
Download the latest release archive and stop, to carry to another machine. |
--dest DIR |
Where downloads go. Default: the current folder. |
--dev |
Sync to the tip of the develop branch instead of the latest release. |
--branch NAME |
With --dev, follow this branch instead of develop. |
--rollback |
Restore the version from before the last update. |
-y, --yes |
Don't ask for confirmation. |
--from cannot be combined with --check, --download-only or --dev. A
downloaded archive is saved as winnow-<version>.zip in the current folder
(or --dest) and left there.
The first update of an install made from a zip cannot tell which old files
the new version dropped, so it removes none; later updates can. After an
update, run pip install -r requirements.txt again before restarting, in
case the new version needs a package the old one did not.
Updating an airgapped machine¶
On a machine with network:
python update.py --download-only --dest /media/usb
Then on the analysis box:
python update.py --from /media/usb/winnow-<version>.zip
A Source code (zip) downloaded from the GitHub release page works with
--from as well. The archive is checked for damage before anything is
written, and an archive that does not contain Winnow, or contains a path
that would write outside the install, is refused.
What an update keeps¶
These are never read, written or deleted by an update:
workspace/,plugins/(apart from its README),sessions/andcases/;.venv/andvenv/;- any case or evidence file by its extension (
.db-winnow,.db,.csv,.tsvand their SQLite companions), wherever it sits in the folder; - the updater's own backups.
Case files stored outside the Winnow folder are not involved at all.
Rolling back¶
python update.py --rollback
This restores the files from the most recent backup in .winnow-backup/
and removes the files that update added. The three most recent backups are
kept. Only program files are rolled back; case files never are. Restart
Winnow afterwards.
Development builds¶
python update.py --dev syncs the install to the tip of develop, with the
same backup, protected folders and rollback as a release update. It is
unreleased code, meant for beta testers. Settings → Updates then shows
the build as Winnow 0.4.0 — develop build <commit>, because the version
number on develop is still the last release's. --dev --check shows the
commit it would sync to, and --dev --download-only saves it as
winnow-develop-<commit>.zip. To go back to a release, use --rollback, or
apply the release zip with --from.
Uninstalling¶
Winnow installs nothing outside its folder except the items in the second table under Where things live on disk. To remove it:
- If you registered file associations, open Settings → File
associations and untick every type first. That removes the registry
entries (Windows) or the desktop entry,
mimeapps.listlines, MIME package and icons (Linux). Deleting the folder first leaves the Open With entries pointing at a program that is no longer there. If you made Winnow a default through Windows' own Open with → Choose another app → Always dialog, Windows keeps that choice; pick another app for those types the same way. - If you stored variables in Settings → Environment, remove them there,
or delete
~/.config/winnow/env(on Windows they are ordinary user environment variables). - Shut Winnow down.
- Move any case files you want to keep out of
cases/, then delete the Winnow folder. Case files you kept in another folder are not affected; delete them separately if you mean to. - Optionally, remove a Linux desktop entry or Windows shortcut you made for
a launcher, a
--browser-profilefolder if you used one, and the browser's stored data for Winnow's address (http://127.0.0.1:8777).