pharo-image-fs exposes a live Pharo image as a local filesystem.
It gives LLM agents and developer tools one normal code-editing path: use rg,
diff, patch, editors, and other filesystem tools against a mounted projection.
Pharo remains authoritative for Pharo semantics: source projection, parsing,
compilation, transactional writes, Tonel export/sync, and critique feedback.
Use this alongside MCP-Pharo when available:
pharo-image-fs is the normal file-editing surface, while MCP tools remain the
better interface for semantic operations such as refactorings, test runs,
critique runs, and repository operations.
- macOS with fuse-t installed
- a Pharo image where the
PharoImageFSpackage can be loaded
On macOS, fuse-t can be installed with Homebrew:
brew install --cask fuse-tIf fuse-t was partially uninstalled and Homebrew still considers it installed, restore the expected files with:
brew reinstall --cask fuse-tThe daemon is built and released from
pharo-image-fs-daemon.
pharo-image-fs downloads the matching prebuilt daemon binary when needed.
Prebuilt daemon binaries do not require Go at runtime, but the platform FUSE
dependency is still required.
Load the project in a Pharo image:
Metacello new
baseline: 'PharoImageFS';
repository: 'github://Evref-BL/pharo-image-fs:main/src';
loadThe baseline groups are:
Core: projection backend and HTTP endpointTests: backend testsdefault:CoreandTests
By default, the Pharo backend downloads the matching daemon release into its
local cache before starting the mount. For local development, set
PHARO_IMAGE_FS_DAEMON to a daemon executable path to bypass the download.
Start the Pharo-side projection endpoint and daemon from the image:
PIFSServer startAndMountOn: 9013To use the default mountpoint with an explicit volume name:
PIFSServer
startOn: 9013
mountNamed: 'pharo-image-fs'To use an explicit mountpoint and volume name:
PIFSServer
startOn: 9013
mountAt: '/tmp/pharo-image-fs' asFileReference
named: 'pharo-image-fs'By default, writes require the target package to belong to a loaded, file-backed Iceberg repository with an existing source directory. Merely having an Iceberg repository entry that lists the package is not enough: the projection must know where to export the changed Tonel source. This keeps filesystem edits coherent with exported source.
Packages loaded from the image itself, system packages, or repositories without an attached checkout are rejected in this default mode with a clear repository ownership error. To allow live-image-only writes, for example in a scratch image, disable the check on the projection backend:
| projection |
projection := PIFSServer
startOn: 9013
mountAt: '/tmp/pharo-image-fs' asFileReference
named: 'pharo-image-fs'.
projection backend requiresRepositoryOwnershipForWrites: falseStop it from Pharo with:
PIFSServer stopOn: 9013Use PIFSServer stopAll to stop every projection server
started through this API.
The mountpoint path is created when it is missing. If the path already exists, it must be a directory.
The daemon requires fuse-t on macOS. fuse-t provides a FUSE-compatible API without requiring the macFUSE kernel extension security flow.
Unmount with the normal macOS unmount command:
umount /tmp/pharo-image-fsIf macOS reports the mount as busy, use:
diskutil unmount /tmp/pharo-image-fsRead and search Pharo code with normal file tools:
rg "projection" /tmp/pharo-image-fs/tonel
sed -n '1,120p' /tmp/pharo-image-fs/tonel/PharoImageFS/PIFSBackend.class.stEdit projected Tonel files with an editor or patch tool. Successful writes are compiled in the live image and exported back to Tonel. If parsing, compilation, or blocking critiques fail, the write is rejected and the previous image state is restored.
Read critique feedback for code edited through the projection:
find /tmp/pharo-image-fs/critiques -type f
cat /tmp/pharo-image-fs/critiques/PharoImageFS/PIFSBackend/write.at..json/critiques does not scan the whole image. Accepted writes through /tonel
refresh critique files for the affected class or extension methods only, and
files appear only when actual Pharo critiques exist.
If a filesystem write fails with a generic editor or OS error, inspect the latest operational error:
cat /tmp/pharo-image-fs/errors/latest.txt/errors is intentionally small and only contains mount/projection operation
failures that are otherwise hard to see from filesystem tools. Pharo critiques
remain under /critiques only.
Inspect the loaded Git checkouts through repository symlinks:
ls -l /tmp/pharo-image-fs/repositories
git -C /tmp/pharo-image-fs/repositories/pharo-image-fs status
cat /tmp/pharo-image-fs/repositories/pharo-image-fs.repository.jsonUse /repositories for repository operations and non-image files such as docs.
For Pharo code, edit /tonel so writes are compiled in the live image and
exported coherently. The default repository-ownership check rejects writes to
packages that are not attached to a loaded, file-backed Iceberg repository. If
the package is visible in the image but has no writable checkout, either attach
it to an Iceberg repository first or explicitly disable repository ownership
checks for live-image-only writes.
The projection is lazy and backed by the live image. Directory listings, stat requests, and reads ask Pharo for the current state as the filesystem needs them, so image-side changes become visible through the mount on later filesystem operations. Filesystem metadata caches are intentionally short; an already-open file handle can still contain the contents read when it was opened.
By default, /tonel exposes every package in the image. A projection can be
limited to package-name prefixes when a large image would make that surface too
broad. Set an inclusion list to expose only matching prefixes, or an exclusion
list to hide matching prefixes. Exclusions take precedence over inclusions.
| projection |
projection := PIFSServer startAndMountOn: 9013.
projection backend
includedPackagePrefixes: #( 'MyProject' );
excludedPackagePrefixes: #( 'MyProject-Legacy' )An empty inclusion list, the default, does not restrict packages. Filtered
packages are treated as absent for listing, reading, writing, deleting,
renaming, and /critiques; this avoids exposing a package through one path
while hiding it through another. Update the lists on the running backend to
change what the mount exposes on later filesystem operations.
/tonel accepts full-file writes for:
- existing
.class.stfiles; - existing
.extension.stfiles; - new
.class.stfiles whose Tonel class definition matches the projection path, creating the package when repository-ownership checks are disabled; - new
.extension.stfiles for existing classes.
/tonel/<Package> directories can be created and removed. Creating a directory
creates a package in the live image when repository-ownership checks are
disabled. With the default checks enabled, creating a package that has no
file-backed repository owner is rejected with a clear error. Removing a
directory removes the package only when it is empty; delete class and extension
files explicitly first.
Editor-safe temporary-file save patterns are supported. Temporary files stay local to the daemon until they are renamed over a real projected Tonel path, where the full file is sent to Pharo as one transactional write.
The write protocol sends full Tonel file contents, but the Pharo backend parses the old and new definitions and only applies definitions that changed. Unchanged methods are left untouched.
Deleting projected code files is supported for .class.st and .extension.st.
Class-file deletion removes the class from the image. Extension-file deletion
removes only extension methods for that class/package.
Moving projected .class.st files across package directories is supported. The
class is moved in the live image. With the default repository-ownership check,
both source and target packages must belong to loaded Iceberg repositories.
Renaming projected .class.st files inside the same package is supported. The
class is renamed in the live image, same-package references are updated, and the
package is exported.
Combined cross-package move plus class rename is supported.
Moving projected .extension.st files across package directories is supported
when the extension filename stays the same. The extension method protocols are
rewritten from the source package to the target package. With the default
repository-ownership check, both source and target packages must belong to
loaded Iceberg repositories.
Renaming projected .extension.st files is not supported.
/critiques is read-only.
/errors is read-only and records a small in-memory history of operation
failures, plus latest.txt.
/repositories is read-only from the projection point of view. Each loaded
Iceberg repository is exposed as a symlink to its real checkout plus a
<repository>.repository.json metadata file. Following a symlink reaches the
normal Git checkout on disk.
/
tonel/
<package>/
<Class>.class.st
<Class>.extension.st
critiques/
<package>/
<Class>/
class-critiques.json
<selector-encoded>.json
errors/
latest.txt
<timestamp>-write.txt
repositories/
<repository> -> /absolute/path/to/checkout
<repository>.repository.json
/tonelis the code editing surface. It mirrors Tonel class and extension files from the live image./critiquesis read-only diagnostic feedback for code edited through the projection. It does not scan the whole image; accepted writes refresh entries for the affected class or extension methods, and entries appear only when actual Pharo critiques exist. Keyword-selector colons are encoded as dots, and binary selector characters use caret escape names, for exampleresponseObjectForOperation.request..jsonand^slash.json./errorsis read-only operational feedback for failed projection writes./repositoriesexposes loaded Iceberg repositories as symlinks to their real checkout directories, plus small metadata files listing the repository name, projection name, path, and loaded packages.
The Go daemon talks to the Pharo endpoint through a narrow JSON protocol:
POST /listwith{ "path": "/tonel" }POST /statwith{ "path": "/tonel/PharoImageFS/PIFSBackend.class.st" }POST /readwith{ "path": "/tonel/PharoImageFS/PIFSBackend.class.st" }POST /writewith{ "path": "/tonel/PharoImageFS/PIFSBackend.class.st", "text": "..." }POST /deletewith{ "path": "/tonel/PharoImageFS/Old.class.st" }POST /renamewith{ "path": "/tonel/PharoImageFS/Old.class.st", "targetPath": "/tonel/PharoImageFS/New.class.st" }POST /mkdirwith{ "path": "/tonel/NewPackage" }POST /rmdirwith{ "path": "/tonel/OldPackage" }
The daemon does not parse or validate Pharo code. It owns mount lifecycle, filesystem callbacks, transport, timeouts, and generic errors. The Pharo backend owns source rendering, write transactions, compilation, critiques, and export.
Matching project releases use the same version number. The Pharo package keeps
its expected daemon version in PIFSVersion.
The useful next improvements are:
- evaluate Linux and Windows mount backends after the macOS workflow is stable.