Runlet 0.2.0
Runlet 0.2.0 lets AI clients run PHP with your approval, shows values inline with magic comments, profiles code with flame graphs, and runs on a Mac with no PHP installed. Website: https://filipac.github.io/runlet/
Highlights
- No PHP? No problem. On a Mac with no PHP (no Herd, no Homebrew, no Docker), Runlet offers a one-click download of its own self-contained PHP 8.5.8 (about 26 MB, for your Mac's CPU). It's checked against a SHA-256 pinned in the app before it's installed. Your installed PHP always comes first. It includes Excimer, so Profile Run works out of the box.
- Magic comments. End a line with
//?to see its value next to the code./*?*/shows an intermediate value,/*?->count()*/a projection, and/*?.*/the elapsed time. Values stream in while the code runs, loops show×N, and hovering shows the full value and every hit. They work on every target, with Run Selection, and can be switched off in Settings. - MCP server for AI clients. Claude Code, Claude Desktop, Cursor and other MCP clients can list your targets and snippets, save snippets, and ask to run PHP (
runlet mcp). Every run asks you first, in a sheet showing the client, the target and all of the code. Only the sandbox can be allowed for a session, production always asks, and SSH never connects silently. It's off by default (Settings ▸ AI Clients). - Benchmark and profile.
Runlet\bench()times functions with min, median, p95, ops/s, memory and a distribution, and compares several side by side. Laravel'sBenchmark::dd()gets the same card.- Profile Run (⌥⌘R) samples your snippet with Excimer and shows an interactive flame graph.
- Open REPL.
php artisan tinker, PsySH orphp -ain a terminal tab, on any target: local, sandbox, Docker or SSH. Production asks every time. - Output, realtime or at once. Choose in Settings ▸ General. Large output is now fast in both modes: 5,000 dumps or 200,000 lines show in under half a second instead of freezing the window. Plain and Raw get a native text view with Find.
- More:
- Explain for captured SQL opens a ready-to-run tab with the query plan request.
- String viewers: JSON trees, searchable text, images and HTML previews.
- Run timing breakdown: bootstrap, execute, memory and queries.
- Sandbox-only Auto-run.
- Snippet descriptions.
- Keep compiled PHP on the server is now on for new SSH profiles.
- Fixes:
- Pane dividers no longer draw behind the title bar and tabs.
- The editor's gutter line no longer draws through banners.
- A Metal validation assert no longer stops runs from Xcode.
Install
Download Runlet-0.2.0.dmg (drag Runlet to Applications) or Runlet-0.2.0.zip. Requires macOS 26 or later, on Apple silicon or Intel.
This build is ad-hoc signed and not notarized, so macOS blocks it the first time. Either right-click Runlet.app ▸ Open ▸ Open, or run:
xattr -dr com.apple.quarantine /Applications/Runlet.appChecksums (SHA-256)
2702ffe07285ee5d6a3fa0dc55fcaa46fa3bc126d2281fbb4bcbfa966949fb23 Runlet-0.2.0.dmg
2888079b7418b8735bee0a2f6ffc994f07f606cb1c64ee00e64584d2c71f8b82 Runlet-0.2.0.zip
Notes
- Early preview. Package tests passed in every pull request in this release; for this build the packaged self-test passed (6 of 6 checks). The UI test suite was not re-run.
- Runlet's own PHP is published separately as the
php-8.5.8-r2pre-release and downloaded only when you click. - Free and open source under the MIT License.
Full changelog for 0.2.0
2026-10-03 — Output realtime or at once (#82)
- Settings ▸ General ▸ Output: Realtime (the default) or At once. At once shows a
run's printed output, dumps, result, errors, magic-comment values, and the inspector's
queries, mail, logs, benchmarks, and profile together when it ends: completed, failed,dd(),
exit, or stopped (what arrived before Stop is shown). The status bar, elapsed time, Stop, and
the Run Log stay live; while it runs the output says "Output appears when the run ends". The
app holds the output, so every target behaves the same, and AI clients over MCP get the full
result in both modes. - It replaces the magic comments' Show values while the code runs switch (#10). A saved
"off" becomes At once; older settings files open with Realtime. - Large output keeps up: before, 5,000
dump()calls or 200,000 echoed lines took minutes to
appear and froze the window. The tab now takes events in batches (at most ten updates a
second, fewer while drawing is slow), printed output is drawn a piece at a time, and the same
runs show within a few hundred milliseconds of finishing. Structured shows the last 5,000
lines of a printed output and the last 1,000 cards (Show All shows every card); Plain and
Raw now use a native text view that appends new output, follows the end while scrolled to the
bottom, and has Find. Plain, Raw, Copy Output, and Save Output always have everything. - New DEBUG step
wait-run[:<seconds>]: waits for the selected tab's run and prints its timings.
2026-10-03 — Specialized string viewers (#7)
- Structured strings offer JSON trees with Copy Pretty, searchable/wrapping text, PNG/JPEG/SVG images, and restricted HTML previews. Long strings open in Text; the original Tree stays available. Recognition and raster decoding are bounded, and incomplete strings retain their truncation notice. See the viewer guide.
2026-10-03 — Run timing breakdown (#9)
- Finished output shows Bootstrap, Execute, and Started alongside labeled total time, peak memory, and query time. Hover the finished row or status to see the complete breakdown, including unavailable phases.
- Completion events preserve runner phase timings through normal, error, cancellation, and transport-close paths. Old completion records remain readable. Query metrics survive Clear Output in the status tooltip and reset for the next run. See the timing guide.
2026-10-03 — Runlet's PHP r2 with Excimer (#79)
- Runlet's own PHP is now build
php-8.5.8-r2, which adds the Excimer extension, so Profile
Run works on a Mac with no other PHP installed. Same PHP 8.5.8 and extensions otherwise. - Updating from r1: an installed older build keeps working, and Settings ▸ PHP ▸ Runlet's PHP
shows "PHP 8.5.8 (r1) installed · Update to r2. Adds Excimer, so Profile Run works." Update
downloads and verifies r2, moves the default PHP and projects' PHP from the old binary to the
new one (also if Runlet quit in between), and removes r1. Nothing downloads without a click. - Remove now clears the default PHP and projects' PHP that pointed at any build of Runlet's PHP.
2026-10-03 — Xcode runs no longer stop on a Metal validation assert (#85)
- Running from Xcode stopped at random on
instanceCount(0) must be non-zero, raised by Metal API Validation while Core Animation replayed a line stroke with nothing to draw. Normal launches were unaffected. TheRunletscheme now runs with Metal API Validation off; turn it back on in Edit Scheme when debugging GPU issues. - The benchmark charts and the flame graph skip drawing at zero size, and the flame graph skips the hover outline on frames too small to show it, so Runlet's own views never ask for an empty stroke.
2026-10-03 — Magic comments (#10)
- Magic comments show values in the editor while the code runs, without
dump()calls or
temporary variables.//?at the end of a line shows the line's value (an assignment's
value, areturn, anecho;✓on a line without a value, such asforeach (…) { //?).
/*?*/right after an expression shows that value,/*?->count()*/(any->or?->
chain) shows a projection while the code keeps the value itself, and/*?.*/shows the
milliseconds since the previous one (or since the snippet started). - Values appear as dim text after the line,
×Nand the latest value for lines that run more
than once. Hovering them (or Edit ▸ Show Inline Value) opens a panel with the value tree and
the list of hits. Magic comments are highlighted, and the gutter marks lines whose comments
ran. Values stream in while the code runs, on every target, and map back to the right lines
for Run Selection. Edit ▸ Clear Inline Values and Clear Output remove them. - Adding magic comments never changes what the code does. The runner inserts probe calls at
byte offsets on the same lines (it never re-prints the code), keeps references
(by-reference arguments,=&,foreach (… as &$v), by-reference returns and yields) and
nullsafe short-circuits, and refuses places where a call would change the code: assignment
targets,isset()/empty()/??operands, constant expressions, the start of"{$…}", and
variables passed to methods that might take them by reference. Refused comments get one
notice and a short reason on their line; the code runs as written. Semantics fixtures run
each snippet with and without its magic comments and require the same results (PHP 8.4 and
7.4). - Limits: the first 100 hits of each comment carry values, later hits are counted with a value
sampled about four times a second, and values stop after 16 MiB per run. Projections run
only for hits whose values are sent, and may query a database. - The next run clears values; a line edited since the run loses its values, and other lines
keep theirs, moved with their text. Values never start a run. - Settings ▸ General ▸ Magic Comments: Show values of magic comments turned off makes them
ordinary comments: runs get no probes on any target, and the editor neither highlights them
nor shows values. Show values while the code runs turned off shows a run's values
together when it ends (also after a failure or Stop); the app holds them, so the runner and
every target work as before. Both are on by default. Profile Run never inserts probes, so
its flame graph shows only the code as written. - A comment after the final expression (
1 + 1; // note) no longer hides its result. - New DEBUG steps for screenshots:
selection:<first>-<last>andinline:<line>|off.
2026-10-03 — Benchmark and profile (#41)
Runlet\bench($callables, $iterations = 1000, $label = null, $seconds = null)measures code
in any snippet, on every target (PHP 7.4+, no extension): a cold call, a short warm-up, then
up to 100,000 calls timed withhrtime(true), stopping early when a callable has used its
time budget (1 s by default, at most 60 s). It returns the numbers in milliseconds and shows
a benchmark card where it ran and in a new Benchmarks section: mean, median, p95, min,
max, throughput, iterations, standard deviation, the first call, the peak memory and the
memory kept per call, a histogram of call times with median and p95 markers, and the times
in run order. An array of callables keyed by label is compared side by side.- Laravel's
Benchmark::dd()shows the same card, with the averages Laravel measures: Runlet
recognizes its dump.Benchmark::measure()only returns numbers, soRunlet\bench()takes
the same arguments. - Run ▸ Profile Run (⌥⌘R, and in the Command Palette) runs the tab like Run, with the
same production confirmation, and samples the snippet with the Excimer extension (wall
time, every millisecond). The Profile section draws a native flame graph: hover a frame
for its function, file and line, samples and share; click to zoom in, Reset to zoom out,
and search to highlight frames. It also lists the hottest functions and copies the samples
as collapsed stacks. Stacks are bounded (4,000 stacks of up to 200 frames), and the view
says when something was folded. - Profile Run is disabled, with the reason, when the target's PHP doesn't load Excimer; the
Command Palette still lists it with that reason. PHP discovery (Settings ▸ PHP), the Docker
profile's Test, SSH Test Connection, and every run report which profilers a PHP loads. SPX
is detected but not used: it profiles only processes started withSPX_ENABLED=1and
writes its reports to files. A PHP without Excimer stops a Profile Run before anything
runs. - The editor no longer marks
Runlet\bench()orRunlet\Inspectoras unknown: the runner
defines them when it runs. - Testing: a disposable runlet-fixtures
profilerservice (PHP 8.4 with Excimer and SPX), and
Tests/Fixtures/docker/fixtures-only-docker, the real Docker CLI limited to Runlet's
disposable containers, for running the app orswift testagainst real Docker. Debug
builds addflame:hover|zoom|search|resetandprofiles:<name>steps for screenshots.
2026-10-03 — Personal snippet descriptions (#52)
- Save Snippet and Edit Snippet support an optional description. Descriptions appear in the Snippets panel and Open Anything, and both search them. Blank descriptions are removed; older snippet libraries load without migration.
- Duplicate and Copy to Personal preserve descriptions. MCP
list_snippetssearches and returns personal descriptions, andget_snippetreturns them when present. Saving, editing, copying, and opening snippets never runs their code. See the guide.
2026-10-03 — Sandbox-only auto-run (#30)
- Sandbox tabs offer Auto-run in the toolbar. Explicitly enabling it shows AUTO and evaluates the whole tab after 800 ms without editor edits; enabling alone never runs the existing code. Changes during a run wait for completion, with no overlapping executions.
- Auto-run is off by default and never saved in sessions or workspaces. Switching targets, loading code, reopening a closed tab, or restarting requires a fresh opt-in. Local, Docker, SSH, and production targets have no auto-run option. Stop, explicit Run, disabling auto-run, and closing the tab cancel pending automatic execution.
- See the sandbox auto-run guide for behavior and validation.
2026-10-03 — MCP server (#43)
- AI clients such as Claude Code, Claude Desktop, and Cursor can use Runlet through
runlet mcp, the bundled tool started with the argumentmcp. Its tools:list_targets,
list_snippets,get_snippet,add_snippet(saves only),run_php(target, code), and
get_last_output. Results read like the output pane: output, dumps, the result, errors
with the line in the code sent, the duration, and the target, plus the same as
structured data. - Every
run_phpshows a sheet in Runlet's window, brought forward, with the client's name,
the target and where it runs, and all of the code. ⌘↩ runs; ↩ or Esc cancels, and the
client hears that nothing ran. A request nobody answers expires after 5 minutes. A request
the client cancels is withdrawn; a run that started finishes in its tab. - "Allow sandbox runs from for this session" is offered only for the Laravel
sandbox. It lasts until that client disconnects or Runlet quits, and Settings can revoke
it. Local projects and Docker applications always ask. - Production targets always ask, with a red warning and Run on Production. The 10-minute
"don't ask again" never applies to AI clients, and approving their runs never grants it. - SSH hosts are never connected silently: the sheet says when pressing Run connects, and a
host that needs a password or a one-time code is refused until you log in with Connect…. - Approved runs open in a tab named after the client, with a note saying who asked, and
are recorded in History. Listing, reading, and saving snippets, starting Runlet, and
restoring tabs never run code. - Settings ▸ AI Clients turns the server on (it is off by default), shows its status and the
connected clients, and gives the Claude Code command and themcpServersJSON for this
copy of Runlet. - The app listens only on a Unix socket in
<data folder>/MCP(folder 0700, socket 0600),
never on the network. Both ends check that the other runs as the same user
(getpeereid), and messages are size-limited.RUNLET_DATA_DIRmoves the socket with the
data. If Runlet isn't running,runlet mcpstarts it in the background on the first call. - The protocol is MCP 2026-07-28 (per-request metadata,
server/discover), with
initialize-based clients served on 2025-11-25, 2025-06-18, 2025-03-26, or 2024-11-05.
No third-party code. runlet --target(and the MCP tools) now also find SSH profiles (ssh:<name>) and take
<kind>:<id>when two targets share a name. Opening a file on an SSH host never connects.
runlet mcpstarted by hand in a terminal explains itself and exits; a folder named
mcpopens asrunlet ./mcp.- Debug builds: the steps
mcp:on|off,mcp-wait,mcp-approve[:session],mcp-decline,
andmcp-state;RUNLET_DEBUG_MCP_APPROVAL_TIMEOUTandRUNLET_DEBUG_APP_PATH;
RUNLET_MCP_NO_LAUNCHfor the tool.scripts/mcp-e2e/driver.pychecks the whole flow
against a hidden Debug build with scratch data. Documentation:
docs/mcp.md.
2026-10-03 — Open REPL in the terminal (#32)
- The Commands pane has Open REPL (also Library ▸ Open REPL and the command palette): the
target's own interactive REPL in a terminal tab, so variables and state carry over from
one line to the next. It works without listing commands first, and it starts only when
clicked: opening, importing, or restoring code never opens one. - Runlet picks the REPL from the project's files: Tinker (
php artisan tinker) when
artisanandvendor/laravel/tinker/exist (the sandbox and Laravel apps), else the
project's PsySH (php vendor/bin/psysh), else PHP's interactive shell (php -a,
titled "PHP shell"). The pane shows which one, e.g. "Tinker · php artisan tinker". - Every kind of target, run the way project commands are: local projects and the sandbox type
the command into your shell in the project folder with the target's PHP; Docker profiles
usedocker exec -itwith the profile's user, working directory, and TMPDIR, in the
resolved container; the Docker sandbox a disposabledocker run --rm -it; SSH hosts an
ssh -tthrough the shared connection (cdto the profile's directory, the profile's
PHP), ordocker exec -itin the container step's container on the server. Docker and SSH
targets choose the REPL on the target, in the sameshthat starts it, and set the tab's
title ("Tinker · app-prod") once they have. The tab stays open after the REPL exits. - SSH connects only on the click, with the usual rules: a password or two-factor host must be
connected with Connect… first (the button is disabled until then). - Production targets ask before every REPL (⌘↩ confirms). The 10-minute grace for snippet
runs never applies, and confirming a REPL doesn't grant it: once a REPL is open, every line
typed into it runs without another question. - Debug builds: a new
terminal:<text>step types into the selected terminal tab (\nis
Return,\ca comma).
2026-10-03 — Explain captured SQL in a new tab (#4)
- Added Explain to query rows, their context menus, and expanded similar-query groups. It prepares a new PHP tab with the captured run target, SQL placeholders, typed bindings, and named connection; opening or restoring it never executes it. Explicit Run keeps the normal production confirmation.
- SQLite requests a query plan; MySQL/MariaDB and PostgreSQL use plain EXPLAIN. Laravel/Eloquent, Symfony, and WordPress use their captured database API. Custom DBAL/PDO templates require recreating the connection explicitly. Incomplete captures and unsupported drivers cannot generate a misleading plan request. See the Explain guide for connection requirements and validation scope.
2026-10-03 — Website: "No PHP? No problem." (#67)
- The website has a new section right after the hero about Runlet's own PHP (#2): Runlet
runs PHP on a Mac with no PHP and no Docker. One click downloads a self-contained PHP
8.5.8 (about 26 MB), checked against the SHA-256 pinned in the app before it's installed,
and installed PHP always comes first. A collage of four app screenshots tells the story
(the banner, the download in progress, Settings ▸ PHP, and a run), in light and dark,
with four numbered steps under it. The nav's Features link now starts there. - Copy that said the sandbox needs installed PHP or Docker now mentions Runlet's PHP: the
sandbox card, the install steps, the requirements, and the FAQ, which also answers "Do I
need PHP installed?". scripts/website-screenshots/shoot-own-php.shmakes the collage: a Debug build on a Mac
that seems to have no PHP and no Docker (RUNLET_DEBUG_HIDE_SYSTEM_PHP=1, a fake Docker
CLI, a scratch data folder at a neutral path), with the release archive served slowly on
localhost so the download shows in progress.own-php-collage.swiftlays out the shots.
render-section.swiftrenders a part of the page with WebKit at a given width and
appearance, and reports anything wider than the viewport.
2026-10-03 — Keep compiled PHP on the server: on for new SSH profiles (#68)
- New SSH profiles start with Speed ▸ Keep compiled PHP on the server turned on: New
SSH Profile, the Profiles window's +, hosts imported from~/.ssh/config, and hosts
first opened from a workspace file. Runs keep PHP's compiled files in a private cache on
the server (~/.cache/runlet/opcache, mode 0700), and edited files are still picked up. - Saved profiles keep their setting. A profile saved without it (by 0.1.0 or earlier, where
it was off unless turned on) stays off, and switching it off is now saved explicitly. - Turn it off in the profile and Runlet writes nothing on the server. It still applies only
to the server's PHP, not with a container step. The help text under the toggle, the SSH
profile header ("only the compiled-PHP cache (Speed) is kept on the server"),docs/ssh.md,
and the website's "nothing is written on the server" lines say so. - Debug builds: a new
scroll:<accessibility identifier>step scrolls an element to the
middle of its scroll view, for screenshots of controls low in a sheet's form.
2026-10-03 — Runlet's own PHP when none is installed (#2)
- When no installed PHP fits, Runlet offers to download its own self-contained PHP 8.5.8
from Settings ▸ PHP ("Runlet's PHP") or from a banner above a sandbox or local-project tab.
It works without Herd, Homebrew, or Docker. It is downloaded only on that click, for this
Mac's CPU only, checked against the SHA-256 pinned in the app, and must run before it is
installed in Application Support. - It is only a fallback: installed PHP (Herd, Homebrew,
PATH) is always preferred, and
Runlet's PHP comes last in the list. It can also be chosen as the default or for a project,
and removed again. - The build is static-php-cli's "common" extensions plus mysqli (WordPress), intl, sodium,
and readline (scripts/php-runtime/craft.yml)..github/workflows/php-runtime.ymlbuilds
it for Apple silicon and Intel, and publishes it as aphp-8.5.8-r1pre-release that never
becomes the "Latest" release. - The banner and Settings show the download's progress. A failed download says why, and
the banner offers Try Again. - The editor's gutter no longer draws its edge line up through the banners above the
editor (this banner, SSH, Docker, and the sandbox image): views stopped clipping to
their bounds by default in macOS 14, so the editor's scroll view and ruler now do. - Debug builds:
RUNLET_DEBUG_HIDE_SYSTEM_PHP=1behaves as on a Mac without PHP, and
RUNLET_DEBUG_PHP_URLfetches the archive from elsewhere (a CI artifact served locally)
before the release exists; it must still match the pinned checksum. New debug steps:
settings-tab:<name>,frame:<window>=<size>, andshot:<name>@<window>.
2026-10-03 — Pull request workflow (#65)
AGENTS.mddescribes how work reachesmain: one branch per issue, a draft pull request
opened early, incremental commits, screenshots for UI changes, and the new
ready to reviewlabel when the work is finished.
2026-10-02 — Keep pane dividers out of the title bar (#1)
- Clip the main window's content and its editor/output area to their bounds so pane backgrounds and dividers cannot draw behind the toolbar, window buttons, or horizontal tab strip. Applies to both horizontal and vertical tabs.
2026-10-02 — Issue-first work tracking and backlog reconciliation (#3)
- Added repository-wide
AGENTS.mdinstructions to track work in labeled GitHub issues before implementation or adding TODOs. - Reconciled release ideas against the changelog/code, archived completed scope in
docs/done-next-release-ideas.md, and linked outstanding and partial scope to GitHub issues. - Updated the historical MVP plan/review and stale SSH command/snippet guide references; preserved historical validation evidence without claiming a fresh test run.