
Security News
GitHub Actions Checkout Now Blocks Risky pull_request_target Checkouts
GitHub Actions checkout now blocks risky pull_request_target checkouts by default to help prevent pwn request supply chain attacks.
BEAM runtime tools for pi — connects to the running Elixir app for live introspection
pi-elixir is the pi bridge for BEAM-native, verifiable Elixir development.
It gives pi a live connection to the running Elixir system, structural Elixir AST tools, supervised BEAM sessions, and resumable eval state. The emphasis is on callable capabilities and verifiers, not only instructions: the agent can inspect runtime state, make syntax-aware changes, and validate them from formatter/compile/test checks up through duplication, static analysis, and architecture/smell checks.
This follows the broader Vibe direction: few model-facing tools outside, rich composable Elixir APIs inside, structured BEAM payloads rendered by pi, and verification through runtime state plus structural analysis.
Real pi TUI output looks like this — compact tool calls, real BEAM status, and session trees rendered in the transcript/widget:
iex case Pi.Agent.parallel(["Reply only: child A ok", "Reply only: child B ok"], name: :review_smoke, timeout: 60000) d…
(70000ms)
%{status: :ok, kind: :parallel, results: ["child A ok", "child B ok"]}
Took 6.8s
✓ review_smoke
2 done
├─ ✓ review_smoke child A ok
└─ ✓ review_smoke child B ok
(ctrl+o to expand)
~/my_app
↑37k ↓156 $0.190 (sub) 6.9%/272k (auto) (openai-codex) gpt-5.5 • medium
⬡ BEAM (embedded)
Instructions are included, but they are not the foundation. The foundation is executable capability: iex into the live app, ExAST-backed structural tools, OTP sessions, project-local plugins/skills, and strict verification gates.
pi-elixir gives the agent concrete operations and checks:
elixir_eval calls. The state is stored as sidecar snapshots next to the pi session, so resume and branch navigation keep the right context.elixir_ast_search and elixir_ast_replace use ExAST patterns, so the agent searches and edits Elixir syntax instead of playing regex roulette.Pi.Session and Pi.Agent run logical child sessions inside the embedded BEAM. Active work renders as a pi widget; completed trees land in transcript once.Pi.LLM and optional Pi.ReqLLM route BEAM calls through pi's current model. pi owns provider/model selection, credentials, streaming, cancellation, usage, and transcript UI; the BEAM side sends structured completion/stream requests over the active bridge.The philosophy is the same as Vibe: compact agent APIs, structured BEAM payloads, runtime state, and Elixir/OTP idioms first. The implementation is pi-native: TypeScript owns tool registration/TUI rendering, while BEAM owns Elixir semantics.
The agent uses iex (elixir_eval) to inspect the live BEAM. Calls render as compact pi tool rows, not giant JSON blobs:
iex alias MyApp.Repo; alias MyApp.Billing.Invoice; stale = Repo.all(...); length(stale)
14
Took 0.1s
The next eval continues from the same IEx-like state:
iex stale |> Enum.group_by(& &1.customer_id) |> Enum.map(fn {id, xs} -> {id, length(xs)} end)
[{"cust_123", 5}, {"cust_456", 9}]
Took 0.1s
That continuity is real state, not prompt memory. On resume/branch navigation, pi-elixir restores the newest matching sidecar eval snapshot.
The agent can ask the live system about supervisors, queues, process state, ETS, logs, and application config:
iex Supervisor.which_children(MyApp.Supervisor)
[
{MyApp.Repo, #PID<0.421.0>, :worker, [MyApp.Repo]},
{MyAppWeb.Endpoint, #PID<0.422.0>, :supervisor, [MyAppWeb.Endpoint]}
]
Took 0.1s
For Elixir bugs, this is the daily win: pi does not have to infer runtime truth from files alone.
ExAST-backed tools show pi-style compact calls and semantic results. The agent can search for code shape instead of text:
Real captured ast grep output:
ast grep defmodule _ do _ end lib/pi/ast.ex · limit 2 · allow broad
1 match defmodule _ do _ end
lib/pi/ast.ex:1 defmodule Pi.AST do @moduledoc "Structured ExAST helpers for bridge tools." ali…
(ctrl+o to expand)
Real captured ast edit dry-run/no-match output:
ast edit Logger.debug(_) → Logger.info(_) lib/pi/eval/snapshot.ex · limit 2 ·…
No matches found.
The structure is Elixir AST. Captures, partial structs/maps, nested expressions, and broad-pattern guards are handled by ExAST, not a regex pretending to know Elixir. When a replacement matches, the same tool row renders syntax-aware edit summaries before textual patch details. For ordinary code review, use AST.diff(changed: true) from eval to orient on changed modules/functions before reading large git diff output.
BEAM sessions render as real pi session trees. This is captured from tmux; names/strings are sanitized only:
iex {:ok, root} = Pi.Session.start(name: :showcase); ...; :ok
:ok
Took 0.1s
○ showcase
3 done
└─ ✓ tests done · done 70 passed
└─ ✓ review done · done LGTM
└─ ✓ research done · done notes ready
(ctrl+o to expand)
~/my_app
↑17k ↓223 R16k CH96.4% $0.102 (sub) 6.3%/272k (auto) (openai-codex) gpt-5.5 • medium
⬡ BEAM (embedded)
For real model-backed BEAM agents, the transcript shape is the same:
iex case Pi.Agent.parallel(["Review API", "Review tests"], name: :review_smoke, timeout: 60000) d…
(70000ms)
%{status: :ok, kind: :parallel, results: ["API ok", "tests ok"]}
Took 6.8s
✓ review_smoke
2 done
├─ ✓ review_smoke API ok
└─ ✓ review_smoke tests ok
(ctrl+o to expand)
Active/running BEAM snapshots are widget-only. Completed root trees are sent once as transcript messages, so you do not get repeated live snapshot artifacts.
The startup screen shows elixir-dev / elixir-new-project as normal pi skills:
[Skills]
... context-management, elixir-dev, elixir-new-project, ...
[Extensions]
... src, webfetch, websearch, ...
Your project can add executable Elixir skills and plugins. The main UX effect is that pi gets your release checklist, Oban conventions, Ecto rules, UI widgets, and slash commands as local trusted project behavior — not as generic prompt text.
pi-elixir deliberately exposes only three model tools:
| Tool | Label | Purpose |
|---|---|---|
elixir_eval | iex | Trusted eval inside the running app. Stateful by default for pi session branches; sandbox mode available for untrusted snippets. |
elixir_ast_search | ast grep | ExAST structural search over Elixir code. |
elixir_ast_replace | ast edit | ExAST structural rewrite with dry-run diffs. |
Everything else is regular Elixir API reachable through eval:
Pi.project()
Pi.logs(tail: 50)
Pi.Bridge.Info.runtime_apis()
Pi.Eval.bindings()
Pi.Eval.forget(:huge_result)
Pi.Eval.reset()
Pi.LLM.complete("Summarize this module")
Pi.LLM.stream("Draft a migration plan")
Pi.ReqLLM.install()
ReqLLM.generate_text(Pi.ReqLLM.current_model(), "Summarize this module")
Pi.Session.start(name: :reviewer)
Pi.Agent.parallel(["Review API", "Review tests", "Review OTP risks"])
{:ok, job} = Pi.Agent.start("Review this module", role: :reviewer)
{:ok, done} = Pi.Agent.await(job, 60_000)
{:ok, text} = Pi.Agent.result(done)
Eval also preloads token-efficient aliases for QuackDB session analytics:
# preloaded: import Ecto.Query; use QuackDB.Ecto
# preloaded: alias Pi.Self, as: Self
# preloaded: alias Pi.CodeMap, as: CodeMap
# preloaded: alias Pi.Quack, as: Q; require Q
# preloaded: alias Pi.Quack.Event, as: E; alias Pi.Quack.SessionFile, as: SF
Self.status()
Self.context("why did sync crash?", limit: 5)
# After non-trivial Elixir edits, reflect before finalizing.
CodeMap.reflect(changed: true)
q = "function_clause"
from(e in Q.errors(),
where: Q.matches(e.id, ^q),
order_by: [desc: Q.score(e.id, ^q)],
limit: 20,
select: %{s: Q.score(e.id, ^q), tool: e.tool_name, content: Q.json_text(e.payload_json, "$.content")}
)
|> Q.table()
This keeps the transcript understandable: the model writes Elixir to control Elixir.
elixir_eval behaves like an IEx/Livebook cell runtime scoped to the current pi execution path:
alias, import, and require persist through Macro.Env;Pi.Eval.bindings/0, forget/1, and reset/0 manage state from inside eval;Physical storage:
<session.jsonl>.pi-elixir/
eval-state/
<toolCallId>.term
<toolCallId>.term.meta.json
When you navigate or resume a pi branch, the extension walks the session branch, finds the newest ancestor eval snapshot, and starts the next eval from that state. New evals write a new immutable checkpoint keyed by the tool call id, so old branch state is not overwritten.
Large or unsafe bindings are handled defensively:
pi Node/TUI
├─ TypeScript extension
│ ├─ registers tools and skills
│ ├─ starts embedded stdio by default, with explicit/discovered HTTP MCP escape hatches
│ ├─ owns TUI rendering and sidecar eval-state paths
│ └─ forwards lifecycle/tool events
│
└─ embedded or external BEAM
├─ Pi.Transport.Stdio / MCP endpoint
├─ Pi.Eval.Supervisor
├─ Pi.LLM.Broker
├─ Pi.Session.Supervisor
├─ Pi.Plugin.Manager
├─ Pi.Skill.Loader
└─ project modules, deps, processes, Repo
The BEAM side emits structured protocol payloads. The TS side renders them in pi style. For example, eval can return an ordered, typed table while staying plain Elixir until the final output helper:
Path.wildcard("lib/pi/**/*.ex")
|> Enum.map(&%{path: &1, bytes: File.stat!(&1).size})
|> Enum.sort_by(& &1.bytes, :desc)
|> Enum.take(8)
|> Pi.table(columns: [:path, :bytes])
Final eval values auto-render when their shape is known (tables for lists of maps/keywords, trees for maps, text for strings). Use Pi.output(value, opts) only when you want to force rendering options such as column order:
Path.wildcard("lib/pi/**/*.ex")
|> Enum.map(&%{path: &1, bytes: File.stat!(&1).size})
|> Enum.sort_by(& &1.bytes, :desc)
|> Enum.take(8)
|> Pi.output(columns: [:path, :bytes])
Use Pi.table(rows, columns: [...]) when you explicitly want to construct table output; otherwise columns are inferred from row keys.
Docs discovery is Enum-friendly for installed modules:
Pi.Docs.entries(Pi.Output)
|> Enum.filter(&(&1.kind == :function and &1.name == :table))
Pi.Docs.get(Pi.Output, :table, 2)
Use source slices when you want read-tool-like context for installed modules:
Pi.Docs.module(Pi.Output)
|> Pi.Docs.function(:table, 2)
|> Pi.Docs.source(context: 25)
Bounded web fetches return structured values and do not expose raw Req:
Pi.Web.fetch!("https://example.com", format: :text)
pi install npm:pi-elixir
Check the bridge from inside pi:
/elixir:status
Use full diagnostics when something looks wrong:
/elixir:doctor
In each Mix project that should use BEAM tools, install the dev-only bridge dependency:
/elixir:install
That adds an exact-versioned dependency such as:
{:pi_bridge, "== <pi-elixir-version>", only: :dev}
The exact version matters: npm pi-elixir and Hex pi_bridge are released together and must speak the same protocol. If you skip /elixir:install, the first Elixir tool call can still prompt to add the dependency.
For new web applications, use Phoenix with Igniter and VibeKit, then add pi-elixir in the project:
mix archive.install hex phx_new
mix archive.install hex igniter_new
mix phx.new my_app
cd my_app
mix igniter.install vibe_kit --agents-md
pi install npm:pi-elixir
For non-web Elixir projects and packages, use Igniter with VibeKit as the baseline:
mix archive.install hex igniter_new
mix igniter.new my_lib --install vibe_kit --agents-md
cd my_lib
pi install npm:pi-elixir
VibeKit provides the project quality baseline (mix ci, Credo strict with ExSlop, Dialyzer, ExDNA, and Reach). pi-elixir provides the live BEAM tools used by agents while they work inside that project. In each generated project, run /elixir:install once to add the exact matching dev-only :pi_bridge dependency.
For local development:
git clone https://github.com/dannote/pi-elixir
cd pi-elixir
pnpm install
cd packages/bridge && mix deps.get && cd ../..
pi install "$PWD"
If you also have npm:pi-elixir installed globally, remove it before dogfooding a checkout to avoid duplicate tool registration:
pi remove npm:pi-elixir
pi install "$PWD"
From an already-running local checkout, /elixir:dogfood performs that switch for you.
Use /elixir:status first for a short bridge summary. Use /elixir:doctor when setup looks wrong; it reports the resolved Mix project, Elixir/Mix availability, pi_bridge dependency status, connection state, embedded startup failures, and a suggested next step.
Common cases:
| Symptom | What to do |
|---|---|
Mix cwd: not found | Start pi from a Mix project directory, or from a supported repo root with a known nested Mix project. |
Elixir is not installed or not available on PATH | Start pi from a shell where Elixir/Mix are available. If you just changed mise/asdf versions, restart pi. |
Stale mise PATH warning | Restart the shell/pi process so removed tool install paths disappear from PATH. |
pi_bridge dependency: missing | Run /elixir:install in the Mix project. |
| Embedded BEAM exited before ready | Fix the Mix/Elixir error shown in doctor, then run /elixir:restart. Wrong Elixir versions surface here as the real Mix error. |
pi_bridge version mismatch | Update the Mix dependency to the exact version expected by the installed pi-elixir, then run mix deps.get. |
Tool registration conflicts with another pi-elixir path | Remove the duplicate install, usually pi remove npm:pi-elixir, then install only the checkout or only the npm package. |
For setup-flow regression testing in this repository:
scripts/manual-setup-flow.sh
It runs tmux/asciinema playground scenarios for non-Mix directories, missing bridge dependency, explicit install, wrong Elixir startup failure, happy path tools, and duplicate package conflicts.
The normal connection path is an embedded stdio bridge started inside the Mix project with Pi.Transport.Stdio.start(). HTTP MCP endpoints are escape hatches for advanced/debug setups.
Resolution order:
PI_MCP_URL, only when explicitly configured for a manually exposed HTTP MCP endpoint.# Advanced/debug only: bypass embedded stdio and use your own HTTP MCP endpoint.
export PI_MCP_URL=http://localhost:4001/mcp
export PI_DISABLE_EMBEDDED=1
Status is transport-focused and actionable: external/embedded/starting/missing/incompatible/offline. Project-specific recommendations live in prompts/skills, not status-bar integrations.
Feature flags are escape hatches for noisy, sensitive, or experimental environments:
| Capability | Default | Escape hatch |
|---|---|---|
Stateful elixir_eval | on | PI_ELIXIR_STATEFUL_EVAL=0 |
| Eval sidecar snapshots | on | PI_ELIXIR_EVAL_SIDECAR=0 |
| BEAM LLM / ReqLLM | on | PI_ELIXIR_LLM=0 |
| BEAM sessions/widgets/control | on | PI_ELIXIR_SESSIONS=0 |
| Project plugins/hooks/UI/commands | on | PI_ELIXIR_PLUGINS=0 |
| Executable Elixir skills | on | PI_ELIXIR_SKILLS=0 |
| Extra-short eval previews | off | PI_ELIXIR_COMPACT_EVAL_PREVIEW=1 |
The package ships pi skills for Elixir work:
elixir-dev — use BEAM eval for runtime introspection, ExAST tools for structural search/edit, LSP for editor semantics, and Mix only for build/test/format gates.elixir-new-project — bootstrap new Elixir packages/projects with strict VibeKit/Igniter-style quality setup.The skill tells the agent how to work idiomatically: prefer runtime truth, inspect installed docs with h/1, exports/1, Pi.Docs.entries/1, and Pi.Docs.get/3, use ExAST patterns for Elixir search/refactors before grep/regex, keep changes verified, and avoid inventing framework behavior.
The release gate is intentionally strict. pnpm run check runs:
Reach and ExAST are not decorative dependencies. They are the direction: agentic Elixir coding should be semantic, structural, and architecture-aware.
Hidden pi command:
/elixir:debug
Writes extension diagnostics to ~/.pi/agent/pi-elixir-debug.log by default.
For event-loop/embedded bridge investigations:
export PI_ELIXIR_DEBUG=1
export PI_ELIXIR_DEBUG_LOG=/tmp/pi-elixir-debug.json
packages/
extension/ # npm/pi package: TS extension, tools, skills, embedded stdio launcher
bridge/ # Hex/Mix package: Pi runtime facade, protocol, eval, plugins, sessions
The npm package is the user-facing pi package. The Hex package is installed into target Mix projects as a dev-only bridge.
Vibe is a BEAM-native coding-agent runtime. pi-elixir ports the most useful ideas into pi:
pi-elixir keeps pi's UI and tool model, but moves Elixir-specific work into the running BEAM: eval state, AST operations, sessions, skills, plugins, and runtime checks.
Prerequisites:
~> 1.20 with OTP 28+Common commands:
pnpm run fmt
pnpm run check
pnpm run check:js
pnpm run check:beam
pnpm run test:integration
pnpm run pack:check
pnpm run check is the release-readiness gate.
pi-elixir gives the pi coding agent a live door into the BEAM: stateful eval, AST tools, and composable runtime APIs.
It is one building block of a larger stack — tools that make AI-generated software checkable: structural search, dependence analysis, duplication and slop detection, session replay, and ecosystem-wide code search. See the Elixir Vibe organization for the rest, and Building Blocks for the Future Web for the thesis, architecture, and roadmap that tie them together.
FAQs
BEAM runtime tools for pi — connects to the running Elixir app for live introspection
The npm package pi-elixir receives a total of 226 weekly downloads. As such, pi-elixir popularity was classified as not popular.
We found that pi-elixir demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
GitHub Actions checkout now blocks risky pull_request_target checkouts by default to help prevent pwn request supply chain attacks.

Product
Socket now supports Custom Roles and Repository Access Permissions so organizations can control who can access specific repositories and actions.

Product
Socket MCP now lets AI assistants review org alerts, investigate threats using the Socket threat feed, and inspect package files in addition to dependency scoring.