Installation
curl -fsSL https://example.invalid/install.sh | sh
Help us improve pal — install it, use it from your agent, and run pal telemetry collect
to share where the language was insufficient. We rely on your feedback to complete the language.
What it can do
With pal you can deploy machine-global policies, and you can also use the built-in policy
authoring tool together with your agent to write task-specific ones — so you can step away from the
churning agent with confidence. Some examples of what is possible:
File permissions
- global
- Allow it access to your project (
./), but disallow writing your.bashrcor reading your.ssh. - task-specific
- For your “fix my merge conflicts” prompt, the agent proposes “edit access only to what's in
git status” as a temporary grant.
Specific API verbs
- global
- Restrict the agent to read-only GitHub API access during planning.
- task-specific
- You prompted “review this PR”, and the agent proposes the permissions
CREATE_REVIEW_COMMENT(owner=me, repo=my-repo),CREATE_PR_REVIEW(owner=me, repo=my-repo), …
Domain filtering
The classic coarse-grained option, for when the verb does not matter: allow github.com, deny
reddit.com.
Limitations
- Shell commands are protected via a sandbox that limits only domains and files. This is done
soundly, but it means fine-grained API verbs used via bash will be rejected even if the policy
allows them. The practical consequence is that today you can allow or deny all
gitactions, but not, say, allowgit addwhile denyinggit commit. - No tracking for sensitive data. Policies cannot state “don't send my sensitive files to Google” (yet). You
would instead deny reading the sensitive files (
.env,~/.ssh) or forbid all access togoogle.com. - We only support the http(s) network protocol at this point. UDP, IPC, domain and raw socket interactions are all disabled.
Is one of these a blocker for you? Tell us — an issue, or the
report_missing_capability MCP tool your agent already has.
How it works
No hidden effects
A script cannot touch a file, a host or a subprocess except by asking for an effect —
fs.write, net.request, proc.spawn — and an effect is a value some
handler decides on (algebraic effects, if you want the term). That is what makes an exhaustive check
possible: there is no second path to the outside world to forget about.
The policy is yours
A .pal/policy.toml in your project says which paths a script may write and which hosts it may
reach. The agent can propose a change and you approve or refuse it; it cannot edit the file. See
writing policies.
Made for your agent
pal init registers pal as an MCP server, so your agent gets a run_pal tool and the
language reference to go with it. One static binary, no runtime — the same one serves the tool and runs
scripts from your shell.
State
pal is a research prototype, and this is the stage at which your experience still changes it. The enforcement works; what is unsettled is whether a language narrow enough to check is still wide enough for the work agents actually want to do, and whether a policy is something a person can reasonably be asked to write.
We are currently investigating how usable pal is by both the agents and users. We ask that you use the telemetry upload every so often to let us know if the agent struggled to use it. And we are also interested in your feedback as to the ergonomics of policy authoring.
Quick taste
The language itself is deliberately unremarkable — Rust-shaped, aimed at the extraction and transformation work an agent would otherwise do in a throwaway script. What is different is that everything it can reach is enumerable, and therefore checkable.
$ pal -c 'fn main() {
let pat = re.compile(r"https?://\S+")
let matches = pat.scan(fs.read("notes.md"))
for m in matches { stdout.write(m.group(0)) }
}'
This is one of the shell forms. Your agent also has access to it through an MCP server. Both are bounded by the same policy.
Writing policies
A policy is a TOML file. pal init leaves a sensible baseline in
your project's .pal/policy.toml — writable paths, reachable
hosts — and every effect a script performs is checked against it as the
script runs.
The policy is yours to edit, but you can leverage your agent for more convenience. The agent can't change the policy, but it can suggest a change which you review and approve or deny. You would see a dialogue like this:
The agent is asking to change what pal scripts may do here.
Agent's reason: I need the closed issues for this milestone to write the changelog.
• allow network requests to api.github.com
Applies until this pal server exits; your policy file is not changed.
This dialogue is natural language, but generated by pal, not by the agent. You can trust its accuracy. Your approval and denial also goes directly to pal, all facilitated by trusted MCP protocol provisions.1 Agent's can request temporary allowances for the current session or you can ask them to permanently edit your policy.
1
Except under opencode, whose MCP client has no confirmation dialog yet: there an approved change is applied when
the agent asks, leaving you a record rather than a say. pal init tells you which you are getting.
Telemetry
Each script run emits one structured record to stderr (also mirrored to a local JSONL file) — outcome, error kind, source hash, model name.
Upload them with pal telemetry collect when you've got a few. You can review (and edit) the
exact data before it's sent. We use this information to make the language more useful;
pal telemetry collect --help spells out what gets shared.
Telemetry collection for sandboxed agents is currently supported only for Claude Code. More platforms coming (next planned is Copilot).
Manual install
The curl | sh line on the install card downloads the right prebuilt binary for your
platform, verifies its checksum, drops it into ~/.local/bin, and offers to register pal
with Claude Code. If you'd rather see what's happening before running it — or you're on a platform we
don't ship a binary for (Intel Mac, Windows, BSD) — here's the same thing, by hand.
Option A: prebuilt binary
Pick the asset matching your platform from
https://pub-c19bb22cdb2d42909de545e416be1e90.r2.dev/latest/ — currently
pal-linux-x86_64, pal-linux-aarch64, or
pal-darwin-aarch64. Each has a sibling .sha256 file with its checksum.
$ ASSET=pal-linux-x86_64 # adjust for your platform
$ curl -fsSL -o pal "https://pub-c19bb22cdb2d42909de545e416be1e90.r2.dev/latest/$ASSET"
$ curl -fsSL -o pal.sha256 "https://pub-c19bb22cdb2d42909de545e416be1e90.r2.dev/latest/$ASSET.sha256"
$ sha256sum -c pal.sha256 # or `shasum -a 256 -c` on macOS
$ chmod +x pal
$ mv pal ~/.local/bin/
Option B: build from source
If you'd rather build, the same source tarball our release pipeline uses lives next to the binaries.
Needs a Rust toolchain (install via rustup.rs) but no submodule fetch
or workspace setup. Swap the path segment for v0.0.0 (or whichever) to pin a
different version; the tarball is named after the same segment.
$ curl -fsSL https://pub-c19bb22cdb2d42909de545e416be1e90.r2.dev/latest/pal-src.tar.gz | tar xz
$ cd pal-*-src
$ cargo build --release -p pal
$ cp target/release/pal ~/.local/bin/
Tell Claude Code about pal, without pal init
Either install path drops the binary on your PATH but doesn't tell your coding agent it exists. In a project,
pal init is the way to fix that. If you'd rather mention pal globally — or just try it before
setting up a project — append a short blurb to ~/.claude/CLAUDE.md instead. This gets you the
shell form only: no run_pal tool and no policy, so the agent decides for itself whether to use
pal, and its other tools remain unaffected. The guard skips the append if the snippet is already there, so
it's safe to re-run:
$ grep -qx '## pal' ~/.claude/CLAUDE.md 2>/dev/null \
|| curl -fsSL https://example.invalid/claude-snippet.md \
>> ~/.claude/CLAUDE.md
Or view the snippet and paste it into a CLAUDE.md of your choice.