cortex
See your files as a graph. Read them, write in them, and open them in your editor — from your app menu, or from the terminal you started in.
Try it in your browser → — the real UI on a frozen copy of this repository, nothing to install.
Point cortex at a folder and it draws everything inside as a living map:
folders, notes, code, PDFs, and the links between them. Click a node to read
the file. Press i and edit it right there. Press Enter and
it opens in Neovim, in your terminal, right where you were.
Then look at it in ways a file manager cannot: watch files light up as an AI agent or a build works through them, replay the project's whole git history as it grows, find the chain of imports from one file to another, and walk a newcomer through the code with a tour that lives in the repository.
No dependencies. No database. No import step. It reads your disk directly, and it is about 14,000 lines of Python standard library and vanilla JavaScript.
Contents
- What problem it solves
- Install
- First run
- Without a terminal
- Reading files
- Editing files
- New, rename, trash
- Today's note, and quick capture
- Following the links
- Searching
- Opening files in your editor
- Working on one project
- Opening another folder
- Ways of looking — live, history, the path, tours, views, disk
- Reading the graph
- Every command and key
- Saving and sharing
- How it works
- Tests
- FAQ
What problem it solves
Note apps like Obsidian, Logseq and Anytype draw a beautiful graph — of one vault you set up for them, containing only markdown. Your actual work is not in one vault. It is scattered across forty project folders, and half of it is code.
cortex maps what is really on your disk:
| Obsidian / Logseq / Anytype | cortex | |
|---|---|---|
| What it can see | one vault you curate | any folder, including your whole home directory |
| Understands code | no | yes — Python, JS/TS, Go, Rust, C, shell |
| Links it draws | [[wikilinks]] |
wikilinks and resolved code imports |
| Edits a note in place | yes | yes, and refuses to save over a change it did not see |
| Notes you have not written | unresolved links | ghost nodes; click one to write it |
| Suggests links you missed | no | yes, from what the notes say |
| Watch files change live | no | yes — agents, builds, git pull |
| Replay the git history | no | yes, and record it as a video |
| Opens a file in Neovim | no | yes, in your real terminal |
| Reads PDFs | via a plugin | built in |
| Setup before first use | create and index a vault | none |
| Runtime | Electron | Python stdlib |
The part no note app can do: cortex is launched from a terminal and keeps
it. Clicking "Neovim" runs nvim on your TTY. You edit, you quit, and you are
back at the graph — still open, still where you left it.
And the terminal is optional. Put cortex in your app menu and it is an app: open a folder, write a note, rename it, throw it away, all from the window. Without a terminal says what changes.
Install
You need Python 3.9 or newer and a browser. That is all. Linux and macOS are both supported.
pipx install cortex-fs
It installs as cortex-fs and runs as cortex. PyPI would not take the
shorter name — something unrelated already has it — and the command is the
one you type every day, so that is the one that kept the name.
Or from the source, which is the same thing without the packaging:
git clone https://github.com/Ayushsinha322/cortex.git ~/cortex
cd ~/cortex
./install.sh
install.sh writes a small cortex launcher into ~/.local/bin pointing back
at this folder. If that folder is not on your PATH, add it:
export PATH="$HOME/.local/bin:$PATH"
Prefer not to install anything? Run it straight from the clone:
python3 ~/cortex/cortex.py
In your app menu
cortex --install-launcher
That is the last time you need a terminal for it. cortex appears in your app
menu (Linux) or in ~/Applications (macOS), and starts like any other app.
install.sh does this for you; with pipx, run it once yourself, or click
add it to the app menu in the window's folder sheet (p).
cortex --remove-launcher takes it out again.
First run
cortex
A window opens showing your home directory. On Chrome, Brave or Edge it is a real app window — no tabs, no address bar. Firefox gets a normal window.
Your terminal stays where it is and prints what it is doing:
___ ___ ___ _____ _____ __
/ __/ _ \| _ \_ _| ____|\ \/ / your filesystem as a brain
| (_| (_) | / | | | _| > < v0.2.0
\___\___/|_|_\ |_| |___| /_/\_\
mapping ~
editors Neovim, nano, vi, VSCodium
url http://127.0.0.1:41277/?t=...
window opened with google-chrome
ready. click a node in the window; actions land here. close the window or ctrl-c to quit.
Leave that terminal alone — it is where your editor will appear. Close the window when you are done, and cortex quits with it; Ctrl-C works too.
Getting around the graph:
- Click a node to select it and open the reader
- Double-click a folder to grow it into the graph (again to collapse)
- Drag a node to move it; drag empty space to pan
- Scroll to zoom toward your cursor
- Arrow keys step from the selected node to the next one that way, following links first
- Press / to search everything, however deep
- Press 0 to fit the whole graph on screen
It opens where you left it. Positions are saved per folder, with the
camera, so a project you open every day is the same map every day instead of
re-settling somewhere new. That is a cache in ~/.cache/cortex/layouts;
deleting it costs one re-settle, and --no-layout turns it off.
Nothing is loaded until you ask for it. A home directory can be hundreds of gigabytes; cortex reads one folder per double-click, so it does not care how big yours is.
Without a terminal
Started from the app menu, cortex has no terminal of its own, and nothing about using it needs one:
- It opens where you left off. With no folder named, it reopens the last folder you had open. Press p for another one.
- You can write without leaving the window. i edits the selected file in place, and n, F2 and Delete make, rename and trash files — see Editing files.
- Windowed editors come first. The editor button is VS Code, Zed, Kate or
whichever windowed editor you have, unless
$EDITORsays otherwise. - Terminal editors still work. Choose Neovim and it opens in a terminal
window of its own — your desktop's terminal, or
$TERMINAL— which closes when you quit the editor. If the editor fails, that window stays open long enough to read why. - Closing the window quits it. A reload does not count; closing does. There is also a quit cortex button in the folder sheet.
- If it stops, the window says so. Rather than buttons that silently do nothing.
- Errors go to a file,
~/.cache/cortex/cortex.log, since there is no terminal to print them to.
An app menu does not read your ~/.bashrc, so a launcher would not normally
know about editors installed in ~/.local/bin or the $EDITOR you set there.
--install-launcher writes down your PATH, $EDITOR and $VISUAL as they
are in the shell you run it from, in a small script beside the menu entry.
Install again after changing them, or edit the script.
Reading files
Select a file and it is rendered in the panel on the right.
| File type | What you get |
|---|---|
| Markdown | fully rendered — tables, task lists, code blocks, [[wikilinks]] |
| Code | syntax highlighted, with line numbers |
| your browser's own PDF viewer, inline | |
| Images | shown; video and audio play |
| CSV / TSV | a scrollable table |
| Notebooks | .ipynb flattened into readable markdown |
| Word | .docx text extracted, with no dependencies |
Clicking a [[wikilink]] grows the graph to that note and selects it. Same
for a relative markdown link. This is how you follow a train of thought without
ever touching a file manager.
Full screen
The panel is 520px, which is fine for a note and useless for a 500-page PDF. Press m, or the full ⤢ button, and the reader takes the whole window — the document gets about 91% of it. esc brings the graph back; a second esc closes the reader.
The file is not re-rendered when you switch, so a PDF keeps its page and scroll position, and at full width your browser's viewer regains its page thumbnails, outline and zoom controls.
Editing files
Press i, or edit in the reader, and the reader becomes the editor. Ctrl-S saves; esc goes back to reading.
● unsaved revert preview save close
─────────────────────────────────────────────────────────────
# Plan
- ship the editor
- [ ] write the release notes
- [ ] |
It is a plain text box, on purpose: what it saves is exactly what you typed. The few keys whose absence you would feel at once are there — tab indents the way the file already does (tabs in a Go file, four spaces in Python), and enter keeps the indentation and carries a list on to its next item, or ends it on an empty one. preview renders a markdown file as it stands, unsaved changes and all. Opened from a content-search hit, the caret starts on that line.
Nothing you type is lost.
- Move to another file mid-edit and the draft is kept. Its node gets a dashed ring, the corner says how many are unsaved, and the file opens straight back into the draft when you return to it.
- Closing the window with a draft open asks first.
- The graph refreshing underneath you leaves the editor alone.
A save never lands on top of a change it did not see. If the file changed
on disk since you opened it — your editor, git pull, a sync client — the save
is refused and you choose: keep mine or take theirs. If the file was
deleted meanwhile, save it again puts it back.
A save keeps the file the way it was. Windows line endings stay Windows line endings, a byte-order mark stays, and the file keeps its permissions, its owner, its hard links and its extended attributes. On Linux the new text is written beside the file and swapped in with one rename, so a full disk or a crash mid-save leaves the old file whole — unless swapping would lose one of those things, in which case it is written in place, as most editors write. A file that mixes line endings says so when you open it: saving evens them out to the one it mostly uses.
It will not open what it could not save faithfully: binary files, text that is not UTF-8, and anything over 2 MB. Those go to your editor, as before.
New, rename, trash
| Key | Does |
|---|---|
| n | new note in the selected folder (or beside the selected file) |
| F2 | rename the selection |
| Delete | move the selection to the trash |
| right-click | all of the above, and more, on any node or on empty space |
A name without an extension becomes a .md note, which opens ready to write
in. Nothing is ever deleted. "Delete" moves to your desktop's own trash —
the one Dolphin, Nautilus and Finder show — so anything thrown away from cortex
can be restored from there. It asks first.
Rename and trash act on what you clicked: a symlink is renamed or trashed itself, never the file it points to. And as everywhere else in cortex, nothing outside the folder you opened can be touched.
cortex --read-only takes all of this away — no editing, no new, rename or
trash — for when you want the guarantee that a browsing session changes
nothing.
Today's note, and quick capture
Press d, or today in the dock, and today's note opens ready to
write in — daily/2026-10-09.md, headed with the date, made on first use. If
the folder already has a Daily, journal or Journal, that is where it goes
instead, beside the ones you already have.
Quick note is the half-second version. Right-click cortex's icon in your app menu, choose Quick note, and a small window takes one line:
● QUICK NOTE → today's note in ~/notes
┌──────────────────────────────────────────────┐
│ ask about the Q3 budget forecast │
└──────────────────────────────────────────────┘
enter adds it to today's note · shift enter new line · esc cancel
Enter adds it to today's note with the time in front —
- 14:02 ask about the Q3 budget forecast — and the window closes. No graph,
no folder to choose. cortex --capture does the same from a terminal or a
keyboard shortcut.
Following the links
Under the actions in the reader is a list of every file connected to this one: what it links to, and what links back to it.
4 connections 3 out · 1 in
→ lexer.rs src/parser/
→ util.rs src/
→ ast.rs src/parser/
← main.rs src/
The arrow says which way the link runs. Green names are note links, blue ones are code imports, the same colours the graph draws them in.
The list is not limited to what is on screen. The index knows every link under the root, so a file can be listed here long before its folder has been grown into the graph. Click it and cortex opens the folders it needs to and selects it. That is the fastest way through a codebase you do not know: open one file, read what calls it, jump, repeat.
Press c to collapse the list when you want the height back. It is hidden in full screen, where you are there to read the file.
Notes you have not written yet
A [[link]] to a note that does not exist is a faint, dashed node of its own —
the note you said you would write. It sits beside the notes that mention it,
and in the reader the link is drawn as a link to nothing yet. Select it and the
reader lists who links to it, with a button that writes it: in the folder of
the note that mentions it, folders and all for [[projects/Roadmap]], opened
ready to type. The unwritten chip hides them all.
Links inside code blocks are not links, and an image or other file named in
[[...]] never becomes a note.
Links you have not made
cortex reads what your notes say, and when two of them keep talking about the same things without ever linking, it says so. Select a note and its suggestions are listed under its connections, with the words they share, and drawn on the graph as dashed lines:
SUGGESTED — THEY TALK ABOUT THE SAME THINGS
≈ costs.md quarter, budget, forecast link ✕
link writes See also [costs](costs.md) into the note — an ordinary
relative link, so it works on GitHub too and cannot point at the wrong note
when two share a name. ✕ says they are not related, and that is
remembered. Nothing is suggested for code, or with fewer than three notes.
Searching
Press / and the box searches file names, anywhere under the root, however deep:
plan.md notes/
design.md notes/
README.md ./
↑ and ↓ move through the results and enter goes to the highlighted one, opening the folders it needs to on the way. With nothing highlighted, enter takes the first, so typing a name and pressing enter simply gets you there. Focus leaves the box as you land, so the graph's own keys work straight away.
Matches are also grafted into the graph with their real folder lineage, so the list tells you which one you want and the graph tells you where it sits.
Press tab, or click the small names button in the box, and it searches inside files instead:
plan.md:2 the budget is fixed
old.md:7 an old budget note
costs.py:41 BUDGET = 10
That is the question you usually have about your own notes — not "where did I put it" but "where did I say that". The same list, the same keys: arrow to one and press enter, and cortex opens the folders it needs to, selects the file, and remembers the line.
The line then follows you into your editor. With a hit selected,
Enter opens the file at that line rather than at the top —
nvim +41, hx file:41, code -g file:41, whichever you use. r
pages to it too. An editor cortex does not have a rule for is opened at the top
of the file, because guessing a flag would stop it opening at all.
Content search uses rg when you have it installed, and falls back to a plain
walk when you do not, so it is faster if you have ripgrep and still works if you
do not. Either way it reads only text files, and never anything .gitignore
excludes.
Opening files in your editor
This is the point of the whole thing.
With a file selected, press Enter or click the blue editor button.
Your terminal — the one you ran cortex in — becomes your editor:
┌─ nvim ~/myproject/DEPLOY.md (quit to return to the graph)
...
└─ back at the graph
Quit the editor and the graph is still there, still live, still on the same node.
Other actions on every file:
| Button | What happens |
|---|---|
| edit | edits it right here, in the reader — see Editing files |
| Neovim (or your first editor) | opens it in your terminal |
| read | pages through it with bat or less |
| editor ▾ | any other editor you have installed |
| shell here | drops you into $SHELL in that folder; exit to return |
| open ⧉ | hands the file to your desktop's default app |
| file ▾ | show in your file manager, copy the path, rename, trash |
| focus | hides every node not linked to this one |
Started from the app menu, read is gone — the reader is right there — and shell here becomes terminal here, which opens a terminal window in that folder.
If you have set $EDITOR or $VISUAL, that is what Enter uses.
You have already told your system which editor you want; cortex is not going to
argue. $VISUAL wins over $EDITOR, and arguments are kept, so
EDITOR="emacs -nw" and EDITOR="code --wait" both work.
Failing that, editors are found on your PATH automatically. Terminal
editors — nvim, vim, nano, micro, helix, kakoune, emacs -nw — run
in the foreground on your TTY. Window editors — code, zed, subl, kate —
are launched detached and marked ⧉.
Working on one project
Pointing cortex at a single project opens the whole project at once — three folder levels deep, smallest folders first — instead of making you click through rings of folders:
cortex ~/myproject
cortex . # just this folder
Name it once and it is a keystroke away forever:
cortex ~/myproject --save myproject # save under a name, and open it
cortex -P myproject # open it again later
cortex --list # what have I saved?
cortex --forget myproject # remove one
Saved as plain JSON in ~/.config/cortex/projects.json, so you can edit it by
hand — or from the window, below. With cortex in your app menu, each saved
project is also a right-click action on its icon.
How much opens on launch:
cortex -P myproject -d 5 # five levels deep
cortex -P myproject -d 0 # nothing; click in yourself
cortex ~/big-repo --max-nodes 300 # stop after 300 nodes
Default is 3 levels for a folder you name, and 0 for your home directory — a home directory is far too big to open eagerly.
What it leaves out
cortex skips caches, build output and dependency trees by name — node_modules,
__pycache__, target, .venv and the rest — because they are noise in a
graph of what you wrote.
That list cannot know your project writes to generated/, so cortex also
reads the project's .gitignore, the file where you already wrote that down.
Negation, directory-only patterns and nested .gitignore files all behave the
way git behaves, so a folder that is clean in git status is clean here.
Pass --no-gitignore when you want to see what git is hiding.
The directory sidebar
You do not have to decide up front. Click ☰ in the top left, or press s, and a sidebar lists every folder in the root. Click one and the graph is rebuilt around just that folder — everything else is not dimmed or filtered, it is gone:
Click another folder to jump straight to it. Close the sidebar and the whole map comes back, exactly as it looked when you launched — closing is always the way out, so it is not a mode you can get stuck in.
There is a filter box for when the root has a lot of folders, and the row you are currently in is highlighted.
Narrowing from the graph itself
Same idea without the sidebar: select any folder node and press o, or click only this. A pill appears in the top bar with the folder's name — click it, or press b, for the whole map again.
Search narrows along with the view, and says so when a match exists but is out of
sight (0 matches for "roadmap" (1 outside this folder)), so a narrowed graph
never quietly hides results. Following a [[wikilink]] that points outside the
folder widens back out on its own.
This is a view, not a permission: the folder you launched on is still the security boundary, and narrowing never lets you reach outside it.
Single projects are where the graph pays off. Across a whole home directory most links have one end off-screen, so the graph looks like a plain tree. Inside one project nearly every link resolves at once. The screenshots above are a 26-file project: 37 nodes and 22 of 22 semantic links drawn.
Opening another folder
Click the folder's name next to the logo, or press p:
OPEN A FOLDER
┌──────────────────────────────────────────┐
│ ~/projects/al │
└──────────────────────────────────────────┘
~/projects/alpha/
↑ up one level home
PROJECTS
work ~/code/work
thesis ~/Documents/thesis missing
RECENT
alpha ~/projects/alpha open now
Type a path and folders complete as you go — tab completes the way a shell does, enter opens. Or pick a saved project or a recent folder. The window reloads around the new folder; nothing from the old one comes along.
save as project names the folder you are in, the same as --save. The ✕
beside a project forgets its name; the folder itself is not touched.
Unsaved edits keep you where you are until you save or revert them, so a switch can never quietly throw your typing away.
Ways of looking
Along the bottom of the window is a dock: live, history, tours, views, disk and today. Each is a different question to ask of the same folder.
Live — watch it change
Press w and every file lights up the moment it changes: green when it is created, amber when it is written, red where it was deleted. A dashed trail joins them in the order they happened, and fades over a minute or so. A list in the corner says what changed and when.
This is the one to leave open while an AI agent works on your project. You
see which files it reads its way towards, what it touches, and in what order,
as it does it — or what a build writes, or what git pull brought in.
- follow flies the camera to each change as it happens, and stops the moment you take the camera yourself.
- reveal brings changed files into the graph even from folders you had not opened.
On Linux the kernel tells cortex what changed (inotify), so it is instant and costs nothing while nothing happens. Elsewhere the folders you have open are checked every second and a half. Nothing is watched at all until you first turn live on.
History — replay the project
Press h in a folder that is in a git repository and a timeline appears. Drag it, and the graph becomes the folder as it was after that commit: what had not been written yet is not there, what has since been deleted or renamed is back where it was. Each commit's changes light up as you reach it, in the same colours as live.
It follows the main line, the way git log --first-parent does: a merged
branch arrives in one step, at its merge, rather than as its own commits
interleaved by date — which would show files together that never were.
space plays it, at 1×, 4×, 16× or 64×, and [ ] step a commit at a time. Everything that ever existed is laid out together first, so nothing jumps about while you scrub.
record plays it from the first commit to the last and saves it as a video, with the date and the commit message drawn on — the life of a project in thirty seconds, for a README or a talk.
The path between two files
How does main.py reach db.py? Select one and shift-click the
other:
PATH cli.py imports server.py imports reader.py only the path ✕
The chain lights up in gold with each step numbered, following imports the way they run when such a chain exists, and any link at all when it does not. only the path hides everything else. Right-click a node for Path from … to here as well.
Tours — walk someone through it
A tour is a path through the project with something said at each stop: start here, this calls that, the interesting part is on line 120. Press T, record a tour, then select files in the order you would show them and write a line about each. Play it back and the graph flies from stop to stop while the note for each one is shown; the arrow keys step through.
A tour is saved as an ordinary markdown file in the project's .tours folder:
# How a request flows
1. [cortex/cli.py](../cortex/cli.py)
It all starts here: **main()** builds the server.
2. [cortex/server.py, line 120](../cortex/server.py#L120)
The routes.
So it is committed with the code, reviewed in pull requests, edited by hand, and every stop is a link that works on GitHub.
Views — come back to how you were looking
Narrow the graph to a folder, hide the kinds you do not want, open the folders you care about, frame it — then v and save this view. Keys 1 to 9 bring the first nine back, exactly as they were. "Frontend", "thesis", "the bug I am chasing".
Views belong to the folder they were made in and live in
~/.config/cortex/views/.
Disk — what is filling the drive
Press u and every node is sized by the space it takes on disk, its
size beside its name. A folder counts everything inside it — including
node_modules, .git and build output, which the graph never shows and which
are usually the answer. Select a folder and the reader lists where its space
goes, biggest first:
4.9 MB in 141 files
docs/ ██████████████████ 2.5 MB
.git/ ██████████████ 1.9 MB not in the graph
cortex/ ██ 232.0 KB
A big folder says "≥" until it has finished counting. Hard links are counted
once, and other drives mounted inside are not counted at all, the way du -x
would have it.
Reading the graph
Colour is the kind of file — folder, note, code, config, document, media, archive — matching the chips along the top bar.
Click a chip to see only that kind. Click "notes" and the graph is your notes; click it again and everything comes back. Folders stay either way, because they are the skeleton the rest hangs from and a graph without them is a cloud of unconnected dots. Shift-click hides one kind on its own, for when a folder full of images is the only thing in the way.
Size is how much is inside: child count for a folder, file size for a file.
A ring instead of a dot means an open folder. A soft glow means the file changed in the last week.
A small dot on the shoulder of a node is what git thinks of it:
| Dot | Meaning |
|---|---|
| amber | modified, and not staged |
| green | staged |
| grey | untracked |
| red | conflicted |
| faint amber | a folder with one of the above somewhere inside it |
The glow is a guess at what you are working on; this is the answer. The selected file says the same thing in words at the top of the reader, next to the branch name in the corner. g turns the marks off for a clean screenshot.
Only the repository above the folder in view is consulted, so a home directory full of projects shows nothing until you narrow to one — the same moment the rest of the graph starts paying off. Nothing outside the folder you mapped is ever reported, even when the repository extends above it.
Edges come in four kinds:
| Edge | Meaning |
|---|---|
| faint grey-blue | the filesystem — a folder to what is inside it |
| green | a note link: [[wikilinks]] and relative markdown links |
| blue | a code import, resolved to a real file on disk |
| purple | a tag, joining every note that carries it |
Tags
A tag is a node in the graph, drawn as a purple ring. That is the point: two notes that never link to each other still meet, at the thing they are both about.
Both ways of writing one are read:
---
tags: [work, urgent]
---
Some thinking about #deep/focus.
Front matter takes an inline list, a comma-separated value or a YAML block
list. In the prose, a #tag is one only after whitespace or at the start of a
line, so # Heading is a heading and example.com/a#frag is a URL. Fenced and
inline code is skipped, because a shell script is not a tag list.
Select a tag and the reader lists the notes carrying it; click one to go there. A tag is not a file, so it has no editor button, no path and no size — there is nothing on disk to open. The tags chip in the top bar hides them all when you would rather see the folders.
Code links are found by actually resolving the import:
| Language | Read from |
|---|---|
| Python | import a.b, from .x import y, from . import a, b |
| JS / TS | import … from './x', require('./x') |
| Go | import "yourmodule/pkg", resolved through go.mod |
| Rust | mod x;, use crate::a::b, use super::x |
| C / C++ | #include "x.h" |
| Shell | source ./x.sh, . ./x.sh |
Go and Rust are resolved the way their compilers see them, not by guessing at
filenames. A Go import names a package, so cortex reads go.mod for the module
path and links to every source file in the imported directory — third-party and
standard-library imports are left alone, because they are not on your disk under
this root. Rust is the opposite shape: mod declares a file and use walks a
path through those files, including foo.rs owning the foo/ beside it.
The index is built in the background as soon as you launch, and links appear as it goes. On a 133,000-file home directory it finishes in about three seconds.
Toggle semantic links with the links button or l.
A tag carried by only one note is not drawn: it labels that note but joins nothing to anything, and a graph full of them is harder to read, not richer. The reader still lists it, and clicking it there brings it in.
Every command and key
You need not remember any of this. ctrl k opens a palette of everything cortex can do, with the key for each beside it, and every file by name:
┌──────────────────────────────────────────────┐
│ hist │
├──────────────────────────────────────────────┤
│ History: replay through git h │
│ file history.py cortex/ │
└──────────────────────────────────────────────┘
cortex [folder] [options]
folder what to map (default: your home directory)
-P, --project NAME open a saved project
--save NAME save this folder under NAME, then open it
--list list saved projects
--forget NAME delete a saved project
-d, --depth N folder levels to open on launch
--max-nodes N stop auto-opening after N nodes (default 700)
-a, --hidden include dotfiles and dot-folders
--ignore a,b,c extra folder names to skip
--no-gitignore show what git hides, too
--no-links skip the semantic index (instant start)
--no-watch do not notice files changing while the window is open
--no-layout do not remember where the nodes were last time
--read-only never change a file: no editing, new, rename or trash
--install-launcher put cortex in your app menu, then exit
--remove-launcher take it out again, then exit
--capture a small window that adds a line to today's note
-w, --window MODE app (default) | tab | none
-b, --browser BIN force a particular browser
-p, --port N pin the port
-V, --version
| Key | Does |
|---|---|
| / | search everything under the root |
| tab | in the search box: file names, or inside files |
| ↑ ↓ | in the search box: move through the results |
| enter | in the search box: go to the highlighted result |
| Enter | open the selection in your editor, in the terminal |
| i | edit the selection right here |
| ctrl s | in the editor: save |
| esc | in the editor: back to reading, keeping any draft |
| n | new note in the selected folder |
| F2 | rename the selection |
| Delete | move the selection to the trash |
| p | open another folder, or a saved project |
| ctrl k | everything cortex can do, and every file, by name |
| right-click | everything you can do to a node |
| shift-click | the chain of links from the selection to there |
| w | live: watch files change as they change |
| h | history: replay this folder through git |
| space [ ] | in the history: play, and step a commit |
| T | tours: play one, or record your own |
| → ← | in a tour: the next and previous stop |
| v | views: save how you are looking |
| 1–9 | go back to a saved view |
| u | disk: size everything by the space it takes |
| d | today's note |
| r | page through the selection in the terminal |
| m | full screen the reader |
| e | expand / collapse the selected folder |
| f | focus mode — hide everything not linked |
| c | collapse / expand the connections list |
| s | open / close the directory sidebar |
| o | only this folder — rebuild the graph around the selection |
| b | back to the whole map |
| l | show / hide semantic links |
| g | show / hide what git thinks of each file |
| 0 | fit the graph on screen |
| arrows | step to the next node that way, following links |
| x | save this view as a picture — X for SVG |
| ? | the shortcut list |
| esc | one thing at a time: a menu, a tour, a path, full screen, the reader |
Optional extras, if you have them: rg for faster content search, bat for
nicer terminal reading, pdftotext to page a PDF in the terminal.
Saving and sharing
Press x, or share ▾ then Picture, and the view you are looking at becomes a PNG in your downloads. X makes it an SVG instead, redrawn from the same numbers rather than captured, so it stays sharp on a slide or in print. Either way it is the view as it stands — pan, zoom, whatever the chips are hiding, labels on or off. t first if you want the shape without the names.
Interactive map is the view as one HTML file that someone else can open in any browser, with nothing to install — the same window, the same graph where you left it, with search, the reader and the tours, but no cortex behind it. It is read-only by construction and does not update. Before it is made you are told what goes in: the map always, and the text of the files on screen only if you tick the box, because a file you have sent is a file you cannot take back.
In the history, share ▾ also offers the replay as a video.
How it works
you type `cortex`
│
├─ a local HTTP server starts on 127.0.0.1, with a fresh random token
├─ a browser window opens pointing at it
└─ your terminal waits, holding your TTY
│
window ──┤ "open this file in nvim"
▼
the terminal runs nvim in the foreground, on your TTY
(started from the app menu: a terminal window of its own)
Six ideas worth knowing:
Nothing is read until you ask. One folder per expansion. Whether you point it at a 20-file project or a 140GB home directory, launch takes the same time.
The graph keeps up with the disk. Write a file in your editor and it
appears in the graph, in its folder, without a relaunch; delete one and it
leaves. The server remembers the folders the window has read and compares their
modification times when the window asks, every two and a half seconds, so an
idle cortex with no window attached does no work at all. The semantic index is
rebuilt on the same signal, but only for a folder that indexed in under a
couple of seconds and never more than once every fifteen — a home directory
takes long enough that it would spend its life re-indexing. --no-watch turns
the whole thing off.
The link index is separate from the graph. It walks the whole folder once in a background thread and the UI adds edges as both ends become visible. You never wait for it.
The layout settles and then stops. Force-directed graphs that simulate forever jitter, and jitter reads as flicker — labels sit right on the collision threshold and blink. cortex cools the simulation to a freeze and then stops drawing entirely, so an idle graph costs no CPU and no battery. Touching anything wakes it.
Nothing is watched until you ask. Live mode's feed of changes, disk usage and the history are each started the first time they are wanted, not at launch, so an ordinary session pays nothing for them — not even inotify watches, which other programs on the machine draw from the same allowance.
Closing the window is quitting. The page says hello as it opens and
goodbye as it closes, and once the last goodbye has gone a few seconds without a
new hello — a reload is both, a moment apart — cortex exits. There is
deliberately no timeout on silence: a browser may freeze a minimised window for
as long as it likes, and a cortex that quit under a minimised window would be
found dead when you came back to it. --window none never quits by itself,
since the page could be anywhere, even at the far end of an SSH tunnel.
Security. This API can start editors and shells and write files, so: it
binds to 127.0.0.1 only, and requires a random token, regenerated every run,
for everything including the page itself. It answers only requests addressed to
127.0.0.1 or localhost, so a web page whose DNS has been pointed at your
machine (DNS rebinding) is turned away before it can read the token out of the
page. Every path is resolved with realpath to prove it is genuinely inside the
folder you mapped before anything touches it, and symlinks pointing out are
refused. Writing follows the same rule; rename and trash check the folder an
item is in, so that a symlink is renamed or trashed itself rather than whatever
it points at.
The token is kept from other people on the same machine too. Any user can read another's command lines, so the browser is never handed the address itself: it is handed a page only you can read, which forwards to the address and is deleted as soon as the window has loaded. The log, which can record the address, is readable only by you. Changes must come from the window: a request from any other site is refused, as is anything that is not JSON, which a page elsewhere cannot send without asking first. A file shown raw — an SVG opened in a tab of its own, say — is sandboxed, so a script inside it cannot act as cortex.
The token never appears on a command line, where ps would show it to every
user on the machine: the browser is handed a page only you can read, which
forwards it to the address and is deleted once the window has loaded. (A
snap-packaged browser cannot read that page, so it is given the address
instead.) Anything that changes something must arrive as JSON from this
window's own origin, which another site's page cannot send. And a file from
disk opened on its own — an SVG from a repository you cloned, in a new tab — is
sandboxed, so whatever script it carries cannot reach the API.
The page is served under a strict Content-Security-Policy, because a markdown
file in a repository you cloned is untrusted input. The renderer escapes HTML;
the policy is the second lock, and it allows no script at all except the two
files cortex serves and one inline config blob carrying a nonce regenerated on
every request. All of that is asserted in the tests.
Layout
cortex/
├── cortex.py run it without installing
├── install.sh
├── pyproject.toml so `pipx install` works; no dependencies to declare
├── cortex/
│ ├── cli.py arguments, saved projects, window launch, action loop
│ ├── scanner.py lazy folder scanning, ignore rules, node building
│ ├── ignore.py .gitignore parsing, the way git reads it
│ ├── links.py the semantic index: links, imports, tags, ghosts,
│ │ and the links your notes are missing
│ ├── grep.py content search, through ripgrep or a plain walk
│ ├── gitstatus.py what git thinks of each file
│ ├── watch.py noticing the disk change under an open window
│ ├── activity.py live mode: every file as it changes (inotify, or a poll)
│ ├── history.py the folder's life, from git
│ ├── du.py what is filling the disk
│ ├── layout.py where the nodes were, per mapped folder
│ ├── reader.py per-filetype preview extraction
│ ├── files.py saving, new, rename and trash
│ ├── places.py saved projects, recent folders, typing a path
│ ├── views.py named views, on keys 1 to 9
│ ├── tours.py tours, as markdown in .tours
│ ├── journal.py today's note, and quick capture
│ ├── snapshot.py the map as one HTML file to send
│ ├── dismissed.py suggestions someone said no to
│ ├── actions.py editor detection, terminal handoff, terminal windows
│ ├── lifecycle.py noticing the window has closed
│ ├── desktop.py the app-menu launcher
│ ├── server.py local HTTP API, token and host checks, path guards
│ └── ui/
│ ├── index.html
│ ├── style.css
│ ├── app.js the graph, the reader and editor, the sheets
│ ├── markdown.js markdown renderer, no dependencies
│ ├── highlight.js syntax highlighter, no dependencies
│ ├── snapshot.js the server, played by data inside a shared map
│ └── icon.svg the favicon, and the launcher's icon
└── tests/
├── run runs everything
├── test_cortex.py scanner, links, reader, actions, HTTP surface
├── test_app.py saving, files, folders, quitting, the launcher
├── test_features.py views, tours, notes, capture, sharing, the routes
├── test_activity.py the live feed, both engines
├── test_history.py git history, odd filenames and all
├── test_du.py disk usage, hard links, partial answers
├── test_suggest.py ghost notes and suggested links
├── harness.js the stub DOM the browser-side tests run against
├── markdown.test.js the markdown renderer
├── render-loop.test.js cooling and repaint gating
├── connections.test.js backlinks, and following one into the graph
├── search.test.js both searches, and the line reaching the editor
├── refresh.test.js the graph keeping up with the disk
├── perf.test.js opening a folder staying cheap as links pile up
├── tags.test.js tag nodes, and their refusal to act like files
├── view.test.js remembered layout, arrow keys, saving a picture
├── edit.test.js the editor, drafts and conflicts
├── files.test.js new, rename, trash, right-click, other folders
├── readonly.test.js --read-only, and the window outliving cortex
├── modes.test.js live, history, disk, paths, views, tours, palette
├── notes.test.js ghosts, suggestions, today's note, sharing
├── capture.test.js the quick-note window
└── snapshot.test.js a shared map, with no server behind it
Tests
tests/run # all of it
python3 tests/test_cortex.py # python only
python3 tests/test_app.py
python3 tests/test_features.py
python3 tests/test_activity.py
python3 tests/test_history.py
python3 tests/test_du.py
python3 tests/test_suggest.py
node tests/markdown.test.js
node tests/render-loop.test.js
node tests/connections.test.js
node tests/search.test.js
node tests/refresh.test.js
node tests/tags.test.js
node tests/view.test.js
node tests/edit.test.js
node tests/files.test.js
node tests/readonly.test.js
node tests/modes.test.js
node tests/notes.test.js
node tests/capture.test.js
node tests/snapshot.test.js
node tests/perf.test.js
859 tests, no framework to install — unittest and plain node. They run on
every push against Python 3.9 and 3.13 on Linux, and 3.13 on macOS. tests/run
also byte-checks every source file, because raw NUL bytes once got into two UI
files and made git treat them as binary, silently breaking diffs and grep.
The browser-side suites drive the real app.js against a stub DOM
(harness.js) with a manual frame pump. That is not for speed: Chrome pauses
requestAnimationFrame in background tabs, so "has the layout stopped
repainting?" cannot be answered from an automated browser — it reports a frozen
canvas whether the code is right or not. Pumping frames by hand gives a real
answer.
FAQ
Does it change my files?
Only when you ask it to: saving in the editor, or new, rename and trash. A save
is refused if the file changed since you opened it, and nothing is ever deleted
— trash is your desktop's trash. Start it with --read-only and it changes
nothing at all. Of its own accord it writes only its own state: saved projects
in ~/.config/cortex/, and recent folders, layouts and a log in
~/.cache/cortex/.
Does anything leave my machine?
No. The server binds to 127.0.0.1, there are no external requests, and no CDN
— the CSS and JavaScript are served from the folder you cloned. The one way
anything leaves is a map you choose to share, which is a file you save and send
yourself, and which holds the text of your files only if you said so.
Can other users on this machine see it? No. It listens on loopback only, every request needs a token that is regenerated on each launch, and that token is kept off the command line and out of anything other users can read — see Security.
Why a browser window and not a terminal UI? Because the graph needs to be pretty and PDFs need to be readable, and neither survives being drawn in text. The browser is the renderer; the terminal is there when you want it, and not needed when you do not.
Which editor does Enter open?
Whatever $VISUAL or $EDITOR says, if either is set and names something
installed. Otherwise, from a terminal, the first terminal editor found on your
PATH; from the app menu, the first windowed one. The editor ▾ menu has the
rest, and the reader's button always shows which one Enter will use.
My terminal editor opens in the wrong terminal app.
Set $TERMINAL to the one you want — TERMINAL=kitty — and install the
launcher again so it picks the setting up. Otherwise cortex uses your desktop's
own terminal (Konsole on KDE, GNOME Terminal or Console on GNOME), then
whatever x-terminal-emulator is, then the first it can find. On macOS it is
Terminal.app.
Where do errors go when there is no terminal?
~/.cache/cortex/cortex.log. From a terminal, cortex still logs its own
internal errors there rather than printing them, because printing would land on
top of an editor running in the same terminal.
Does it work on macOS?
Yes. Terminal handoff, the graph and the reader all behave the same; open ⧉
uses open instead of xdg-open. The launcher is a small app bundle in
~/Applications, terminal editors open in Terminal.app, and trash goes through
Finder, so Put Back works — the first time, macOS asks whether your
terminal may control Finder.
What are the dashed things on the graph? A dashed node is a note something links to that has not been written yet. A dashed ring around a node means it has unsaved changes in the editor. With live or history on, a dashed outline in red is where a file was just deleted, and the dashed gold line is the order things changed in. Dashed cyan lines are suggested links.
Live says "checked every 1.5s", not "instant". That is the fallback for when the kernel's change notifications are not available — on macOS, or on Linux when the per-user inotify limit is used up by other programs. It still sees every change in the folders you have open, a second or so later.
Can I use it over SSH?
Run cortex -w none, forward the port (ssh -L 41277:127.0.0.1:41277), and
open the printed URL locally. Editor actions run on the remote terminal, which
is usually what you want.
It says no browser found.
Use -w none and open the URL yourself, or point it at a binary with
-b firefox.
The graph is a plain tree with no green or blue links.
You are probably looking at your whole home directory, where most links have one
end off-screen. Open a single project (cortex ~/myproject) and they appear —
or select that project's folder in the graph and press o.
I opened my home directory but only want one project now. Press s for the sidebar and click the project, or select its folder in the graph and press o. No need to quit and relaunch. Closing the sidebar — or b — brings the full map back.
Can I map a folder outside my home directory? Yes — any folder you can read. That folder becomes the boundary, and nothing outside it can be opened.
Licence
MIT. Do what you like with it.
Metadata
Release files for cortex-fs 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| cortex_fs-0.2.0.tar.gz | 275.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cortex_fs-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 476.5 kB
Release files / cortex_fs-0.2.0.tar.gz
| Download URL | cortex_fs-0.2.0.tar.gz |
|---|---|
| Size | 275.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2f93326fdf3b4dcf03726e3f338f66ff80f15556d356eabaca93cffe32306b6b
|
|
BLAKE2b-256 checksum How to use checksums |
ee37d47514508ab6cf72b14c57b8039c51cf66e5c9e4003fe9875dceafc923e6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency logRelease files / cortex_fs-0.2.0-py3-none-any.whl
| Download URL | cortex_fs-0.2.0-py3-none-any.whl |
|---|---|
| Size | 201.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
508a94632025a7883a8d6f1ab74ab34b951bf12f20808394dc02572507c3f140
|
|
BLAKE2b-256 checksum How to use checksums |
39c14be634fcf025c022eec17e9d08822dcb973c50daf6d515866527889c3e1d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency log