--allow-write while every write is validated, contained, and applied atomically.
SafeEditPipeline runs a propose → approve → apply workflow: it confines writes to the workspace, rejects symlink escapes, claims the apply transition atomically, and writes via a temp-file rename.
Quick Start
1
Enable writes on the ACP server
--allow-write routes them through the safe-edit pipeline.2
Propose, approve, apply from Python
Guarantees
Workspace Confinement + Parent-Symlink Defence
_validate_path() rejects any target that escapes the workspace or traverses a symlink.
It raises ValueError with one of these exact messages (users grep for them):
- resolves outside the workspace —
Path {..} is outside workspace {..} - traverses a symlinked parent —
Refusing to write through symlinked parent {..} (possible containment bypass) - traverses a parent that exists but is not a directory —
Parent {..} is not a directory - is itself a symlink —
Refusing to write through symlinked target {..}
resolve() follows symlinks, an attacker can swap an intermediate workspace-owned directory for a symlink between the two calls so a later pathname-based write escapes containment. Rejecting any symlinked parent means the only writable path is one made entirely of real directories the workspace owns.
Atomic APPROVED → APPLIED Transition
apply_edit() claims the APPROVED status atomically so two concurrent applies of the same proposal cannot both succeed.
The pipeline holds an RLock (self._lock) around _proposals mutations and status transitions. apply_edit() checks the proposal is APPROVED and flips it to APPLIED inside the lock (a tentative claim), then does the disk work outside the lock; a second concurrent apply_edit() finds the status is no longer APPROVED and returns False.
Atomic File Write
The final write goes through_atomic_write_text (a sibling temp-file + fsync + rename), so an interrupted apply never leaves a half-written or truncated file.
Per-Workspace Pipeline
get_safe_edit_pipeline(workspace=...) returns a distinct SafeEditPipeline per resolved workspace so concurrent agents cannot leak proposals across workspaces.
_pipelines: Dict[Path, SafeEditPipeline] keyed by the resolved workspace path. A previous single global silently bound every caller to the first caller’s workspace; the per-workspace cache removes that cross-talk.
Full Flow
Error Taxonomy
Best Practices
Scope the workspace tightly
Scope the workspace tightly
Set
--workspace (or ACPConfig(workspace=...)) to the smallest project root that covers your edits so _validate_path() rejects anything outside it.Keep --allow-write opt-in
Keep --allow-write opt-in
ACP is read-only by default. Add
--allow-write only for sessions that need to edit files, and prefer manual approval for risky changes.Do not pre-create symlinks in the workspace
Do not pre-create symlinks in the workspace
Symlinked parents and targets are rejected by design. Use real directories so writes are not refused as possible containment bypasses.
Use one pipeline per workspace
Use one pipeline per workspace
Call
get_safe_edit_pipeline(workspace=...) rather than sharing a single instance so concurrent agents keep separate proposal state.Related
ACP
Connect IDEs and editors to PraisonAI agents.
Security Best Practices
Injection defense, audit logging, and protected paths.
Auto-Generator Safety
Safe defaults for
praisonai --auto.Tools
Add custom tools to your agent.

