Repository navigation
Releases: Galfurian/filo
Release list
filo 0.4.3
An agent that runs its commands in a git worktree of the project can now answer. If you use filo with Codex or agy in worktrees, upgrade: until then such an agent was told that a recipient who was connected was not.
Fixes
-
A registered session kept looking at the scope of the directory it ran in. A scope is the directory a command runs in, and an agent does not always run its commands where its scope is. A Codex session registered in a project, whose commands ran in a worktree of it (
.worktrees/NAME), looked at the scope of the worktree, found nobody (filo whosaid none) andfilo sendreportedrecipient 'manager' is not connected, while the manager, who wrote to the project, always reached it. Only--scopemade it work, and only an agent that had been told to say it. This is how scopes have been chosen since the first version.Now
who,send,status,show,leaveandrenamework in the scope where the session that runs them is registered, whichever directory its shell is in. The session is the one of the client that runs the command ($CODEX_THREAD_ID,$ANTIGRAVITY_CONVERSATION_ID, or the id variable of an adapter), and it is used when the current directory is another scope and holds no registration of it.sendalso uses the one scope where its--fromname is a member, which covers a listener.
Changes in behaviour
- A command that works in another scope than its directory says so, on stderr:
filo: working in X, not in Y: this session is registered there (--scope chooses another).whonames the scope it looked at, in the text and in the JSON. - It is never a guess: when two scopes hold the session, or the name, nothing is inferred.
--scopealways wins.join,registerandwatchkeep using the directory: they create the membership or read what a listener here keeps. - A broken adapters file no longer stops a command that does not need it (
sendwith a name that is a member of a scope, for example).
Upgrade
filo upgrade, or the one-line install again. A session that was started before the upgrade runs the old version until it is restarted; an agent in a worktree can use --scope /path/of/the/project until then.
Security posture
Unchanged from 0.4.2: get.sh is signed with Sigstore, covered by the build provenance and listed in SHA256SUMS, like the archive.
filo 0.4.2
Three fixes from a second review. The first matters if you start several sessions at once: two of them could end up with the same name, and one could no longer be reached.
Fixes
- Listeners that joined at the same moment could get the same name.
joinlooked for a name in use, removed the files of that name and bound a socket in three steps, and the metadata that says a name is in use is written only later. The second listener saw nothing, removed the socket that the first had just bound and bound its own: both wereclaude, the first could not be reached, and what was sent toclaudewent to the other. Eightjoin --as samestarted together gave three different names, some of them twice. A name is now held with a lock (NAME.lock) for as long as its listener lives, a detached one included; the system takes it back when the process ends, however it ends. The checks that existed (a registration of that name, a listener of an older filo) stay. - A client that sent a byte now and then could hold a listener for as long as it liked. The timeout of a read started again with every byte, and a listener serves one connection at a time: while it was held nothing else was served, the spool was not delivered and the listener did not notice that its parent had ended. The whole message now has to arrive within the time, for the listener and for a client waiting for an answer. (It takes a process of your own user, which
SECURITY.mdputs out of scope; this is robustness.) filo upgradejudged an address by how it began.http://127.0.0.1:80@example.org/began like the local address that is allowed for a mirror and the tests, and is example.org. The address is now parsed: https, or http to 127.0.0.1 or localhost on a port, and never one that names a user.upgradestill insists on aget.shstamped for the release, shows its dry run and asks before it runs it.
Changes in behaviour
None that you would notice. There is one more small file for each name that has had a listener, NAME.lock, in the runtime directory (0600), like the other state; it does not need to be cleaned.
Upgrade
filo upgrade, or the one-line install again. A session that was started before the upgrade runs the old version until it is restarted.
Security posture
Unchanged: get.sh is signed with Sigstore, covered by the build provenance and listed in SHA256SUMS, like the archive.
filo 0.4.1
Fixes for nine problems found in a review of 0.4.0, two of which lost a message that its sender had been told was delivered. If you use filo with a detached listener (the Claude setup), upgrade.
Fixes
- A big message with non-ASCII text could be reported as delivered and then be lost. A listener keeps what it receives in a file, and a sender leaves one in the spool when the recipient is away, with every non-ASCII character escaped, which takes up to six times the size of the message; a file above the size of one frame was refused. A message of 3 MB of Chinese text was "sent directly" and then
filo showcould not find it andfilo watchnever printed it; one that waited in the spool was deleted when its recipient came back. A file may now hold what a frame holds, escaped. filo sendrefuses a message that is too big to carry (about 4 MB of text) and says so, instead of falling back to a spool that would have thrown it away.filo watchskipped a message whose id was older than the last one it printed. An id is made by the sender before it delivers, so a slow sender, or the spool (every message from a sandboxed sender goes through it), could deliver a message after one sent later. It was saved and never printed. The cursor now says when a message was received, not which id it had; the cursor of an older filo is understood.- A bad adapters file stopped
filo who,filo joinand a message to a listener that was away, for everyone, even with nobody registered, andjoinleft its socket behind. The error is printed to stderr and the command goes on. - A name of 300 characters, a directory that had been removed, or a
--scopetoo long to look up ended in a traceback. Names are limited to 64 characters, and any remaining file system error is a one-line message with exit status 1. filo join --parent 0made a listener that never ended, becausekill(0, 0)always succeeds.--parentmust be above 0.- The listener wrote an acknowledgement file named after the id that a raw client of its socket sent, so
../../xwrote outside its directory and a long id made the answer an error after the message had been shown. It now does so only for an id that filo could have made. (It needs a process of your own user, whichSECURITY.mdputs out of scope; this is robustness.) - A spooled message was deleted before it was delivered, so a listener that ended in between lost it. It is deleted after, so one that is cut short is delivered again (a message may repeat; one is never lost). A message that cannot be shown at all is still dropped, or the listener would fail on it at every start.
- A delivery command was given every variable in the registration file, not only the ones its adapter captures. It now gets only those (defence in depth: it takes a process of your own user to write the file).
Also
- Old acknowledgements are pruned once a minute, with the inbox, not twice a second by every listener.
Changes in behaviour
A name longer than 64 characters, a message of more than about 4 MB of text and a --parent of 0 or less are now errors; they did not work before, and failed late or not at all. Nothing else a user does changes.
Upgrade
filo upgrade, or the one-line install again. A session that was started before the upgrade runs the old version until it is restarted.
Security posture
Unchanged: get.sh is signed with Sigstore, covered by the build provenance and listed in SHA256SUMS, like the archive.
filo 0.4.0
A Claude session that leaves and rejoins no longer wakes everyone else, and a message says which scope it comes from correctly. If you use Claude with filo, read the first section: a session that is already running keeps the old behaviour until it runs /filo again.
Changes in behaviour
- The listener of a Claude session is no longer its Monitor. A Claude Monitor ends after at most 30 minutes. While the listener was the Monitor, each end told every other session that it left, and each restart that it joined, and every one of those notices woke them: one session's Monitor cost a turn to all the others, twice every half hour. The skill now starts
filo join --as NAME --detach --parent "$CLAUDE_PID", a listener in a process of its own that ends with the Claude session, and a Monitor onfilo watch --as NAME --preview 400, which prints one JSON line per message and nothing else. Starting the Monitor again changes nothing for anyone and prints what came in meanwhile. A session started before the upgrade keeps its Monitor onjoinuntil it runs/filoagain. - A message now says which scope its sender belongs to, not the directory it happens to run in. An agent registered in a project that moved to a git worktree used to be reported as a stranger, with a reply command that failed. A
messageevent has"foreign": trueandorigin(the sender's scope) only when the message comes from another scope, and itsreplycommand then carries--scope; thescopefield is gone from events, and an ordinary event is about 150 characters shorter. Keyword scopes work too. Codex, agy and OpenCode are told "sent from SCOPE (another scope)" in the same case. filo leave --name NAMEstops a listener (it was an error before).- A message sent to a detached listener counts as delivered when the listener has it, whether or not a
watchis running. - The skill no longer tells an agent to stop at a message from another scope and wait for you. It says who wrote and from where when it reports, and then decides as for any message, asking you first only when the message asks it to change files, run commands, send something elsewhere or do something outside its task. Nothing in a message is from the user, as before.
New
filo join --detach --parent PID: a listener that goes on by itself, in a session of its own, and ends withPIDor withfilo leave --name NAME. It is not a service: no port, its own user only, and only when a session started it.filo watch --as NAME [--preview N]: the messages of a detached listener, one JSON line each, until it ends ({"kind":"ended"}). A message printed just as the previouswatchended can come again with the same id: filo may repeat one and never loses one.- The README is ordered by use: quick start, profiles, commands, install and troubleshooting first; how it works, the clients one by one and the rest after, under a note that says the later sections are not needed to use filo.
Upgrade
filo upgrade, or the one-line install again. A session that was started before the upgrade runs the old version until it is restarted.
Security posture
Unchanged from 0.3.1: get.sh is signed with Sigstore, covered by the build provenance and listed in SHA256SUMS, like the archive. SECURITY.md now also describes what a detached listener is and is not, and says that home and origin are what the sender's process reports, as --from is.
filo 0.3.1
Fixes for eight problems that property-based tests found in how filo reads what other processes write to it. If you run filo, upgrade: one of them let a single frame sent to a listener end that listener.
Fixes
- A deeply nested JSON frame ended the listener. A document nested tens of thousands of levels deep (a 100 KB frame is enough) raised
RecursionErrorin the reader of a socket frame and of a metadata file, and nothing caught it. Any process of yours could stop a listener that way (the sockets and files are private to your user, so no one else could). It is now a protocol error, like any other bad frame. Infinityin a number field raised an exception that the callers did not catch. It is now an ordinary bad value.- A name could be
.or.., which as a directory underspool/andinbox/is the directory above it. Both are refused now. - A message whose
originheld a NUL character was lost instead of delivered. An origin that cannot be checked now counts as coming from another directory. - The reply command put the sender in unquoted, so a sender with a space or a semicolon in its name split into several words. It is quoted now; valid names are written as before.
- A message that contained
{scope},{session}or{message}was altered on its way to a client run by an adapter, which replaced the placeholders one after the other. They are replaced in a single pass, and the message arrives as it was written. - A shortened event of an empty body was marked
truncated, with nothing cut. --scope ~nobody, for a user that does not exist, ended with a traceback. It is a keyword now, like any other word.
Changes in behaviour
. and .. are no longer valid names (filo join --as .. is an error). Nothing else a user does changes.
Tests
tests/test_properties.py states what must hold for any input, with Hypothesis (a dev tool only: filo still has no dependency). Each of the eight is now an @example of the test that found it. HYPOTHESIS_PROFILE=explore searches harder; CONTRIBUTING.md says how.
Upgrade
filo upgrade, or the one-line install again. A session that was started before the upgrade runs the old version until it is restarted.
filo 0.3.0
A message can no longer be lost to a shortened notification, and a message now says where it came from. If you use Claude, read the first section: a running Monitor has to be started again to get the fix.
Changes in behaviour
- The Monitor of a Claude session starts with
--preview 400. A Claude Monitor shows only the start of a long line (about 500 characters) and cuts it without a mark, so the end of a message, often the instruction, could be lost without the reader knowing. The skill now starts it asfilo join --as NAME --json --preview 400, and an agent that sees"truncated": truereads the whole message withfilo show ID. A Monitor that is already running keeps the old behaviour until the session runs/filoagain. - In every
messageevent the body comes last, afterfrom,idandreply, so a client that cuts the end of a line still shows who wrote and how to answer. Only the order of the keys changed. - A message says where it was sent from. The event has the
scopeof the listener, theorigin(the directory the sender ran in) and, when that is another directory,"foreign": true; itsreplycommand then carries--scopewith that directory. For Codex, agy and OpenCode the text says "sent from DIRECTORY (another scope)". The skill tells an agent to pass a foreign message on to you and wait.originis what the sender's process reports, as--fromis: it lets an honest agent notice that a message crossed scopes and protects against nothing else. - The
participantsevent ofjoin --jsonlists registered sessions too (registered). It was[]when only a Codex or agy session was there.
New
filo show IDprints the whole text of a message you received (--jsonfor the record). Each listener keeps what it receives, privately, for 24 hours.filo join --json --preview Ncuts an event longer than N characters (at least 100) and says so:"truncated": true, thelengthof the whole message andshow, the command that prints it. Without it an event is whole, which the OpenCode plugin needs.filo who --all [--json]lists every scope that has someone in it. A scope shows up once a session has joined or registered with this version.
Upgrade
filo upgrade, or the one-line install again. A session that was started before the upgrade runs the old version until it is restarted.
Security posture
Unchanged from 0.2.0: get.sh is signed with Sigstore, covered by the build provenance and listed in SHA256SUMS, like the archive. SECURITY.md now also describes what a listener keeps and what origin is worth.
filo 0.2.0
Install and upgrade filo without a checkout. This release is the first one that carries get.sh, so the one-line install works from here on.
New
-
One-line install and uninstall.
get.sh, attached to each release, downloads the release archive over https, checks its SHA-256, unpacks it into~/.local/share/filo(FILO_HOMEchanges that) and runs theinstall.shinside, which says which links it makes before it makes them.curl -fsSL https://github.com/Galfurian/filo/releases/latest/download/get.sh | sh curl -fsSL https://github.com/Galfurian/filo/releases/latest/download/get.sh | sh -s -- uninstall
Both take
--dry-run(sh -s -- --dry-run, or downloadget.shand runsh get.sh --dry-run). Uninstall deletes only a directory that holds the markerget.shwrote, and refuses/, your home directory, a relative path and a link../install.shfrom a checkout works as before. -
filo upgrade.filo upgrade --checksays whether a newer release exists, by looking where/releases/latestpoints.filo upgradefetches theget.shof that release, refuses it unless it is stamped for that version, shows its dry run, asks (or takes--yes) and runs it for the directory this filo runs from. A checkout is told togit pull; a copy that did not come fromget.shis refused. -
An opt-in notice of a new release. With
FILO_CHECK_UPDATES=1,filo wholooks at most once a day, for two seconds at most, and adds one line on stderr when a newer release exists. It is off unless you set it, silent when offline and never printed with--json. The skill tells agents to pass the notice on to you and not to runfilo upgradethemselves.
Behaviour to know about
filo upgrade and the optional notice are the only places where filo uses the network; messages never leave the machine. FILO_RELEASES_URL accepts https only. SECURITY.md has a section on this.
Security posture
get.sh is stamped by the release workflow with the version and the SHA-256 of the archive, so a copy from a release can only fetch that archive; an unstamped copy refuses to run. It is signed with Sigstore (get.sh.sigstore.json), covered by the build provenance and listed in SHA256SUMS, like the archive. The hash and the archive come from the same release, so someone who can change a release can change both: check get.sh with the signature or the provenance first if that matters to you (SECURITY.md, "Checking a release").
The project is registered for the OpenSSF Best Practices badge, shown in the README.
filo 0.1.1
Fixes that came out of an audit of 0.1.0, one of them a change in behaviour for Codex. If you use Codex with filo, read the first section.
Changes in behaviour
- filo no longer guesses which Codex session you mean. Before,
send --to codexwith nobody registered, andregisterwithout a thread id, took the newest thread of the directory from Codex's own database. Both are gone. A message goes only to a session that registered itself: run/filoin Codex, andregisterneeds$CODEX_THREAD_IDor--session. Sending to an unregisteredcodexis now an error that says so. - A registration is forgotten only when none of the client's processes run any more. It used to keep the newest ones when fewer processes ran than there were registrations, which could keep the dead session and drop the live one. When it cannot tell which one ended it now keeps them all.
- filo refuses a runtime directory it should not trust: a link, a directory that is not yours, or one that is open to others and already holds a link or something of another user's. The same checks apply to the directory of every scope, and metadata files are read without following links.
Fixes
- Two sessions registering at the same moment could end with the same name. Eight simultaneous registrations lost or doubled a name in 18 runs out of 40; now it is 0 out of 40. A name is claimed by creating its file with a link, which only one process can do.
renameclaims its new name the same way.
New
filo --version.CONTRIBUTING.md, a Code of Conduct, bug and feature forms and a pull request template.- CI also checks the workflows with actionlint and zizmor.
Security posture
main is protected by a ruleset that applies to everyone: a pull request, five passing checks (Linux 3.10 and 3.12, macOS and CodeQL for Python and JavaScript), a linear history. Private vulnerability reporting is on; see SECURITY.md.
The archive is signed with Sigstore and has a build provenance attestation, as for 0.1.0.
filo 0.1.0
First release of filo: direct messaging between interactive AI coding sessions in the same directory, without taking the sessions over.
What it does
- Connects live sessions of Claude Code, Codex, Antigravity (agy) and OpenCode. Each stays an ordinary interactive session; you keep typing to it while it works with the others.
- Sessions message each other by name (
filo send --to librarian ...), and you start each one with a role:/filo manager,/filo worker,/filo researcher,/filo librarian, or your own profile in~/.config/filo/profiles. - A session that is away gets its messages when it rejoins; one that runs
filo leaveis forgotten. - Any other client can be added from a JSON file, with no code change.
How a message gets in
- Claude: a Monitor on
filo join. Codex:codex queue. agy:agentapi send-message, using the session's own address and token. OpenCode: a plugin that startsfilo joinand turns each message into a prompt in the same session.
Install and remove
./install.sh --dry-run prints exactly what it will do; ./install.sh only makes symbolic links, needs no sudo and changes no permissions. ./uninstall.sh removes only those links.
Systems
Used day to day on Linux (Ubuntu 24.04 under WSL2). The tests also run on macOS in CI; the client integrations have not been tried on a Mac. Windows needs WSL.
Security
One user on one machine; not a network service. The runtime directory is private to the user and filo refuses one that is not. A message is untrusted input to a model, and --from is a label, not an identity. The agy and OpenCode integrations use interfaces that are not a stable public contract, so a client update can break them. The full model is in SECURITY.md. The archive is signed with Sigstore and has a build provenance attestation.