DEV Community

Giving LLMs access to a bash terminal is terrifying

Recently, I was testing an autonomous AI agent in a local directory. I gave it a multi-step coding task and a terminal tool so it could run its own tests. Everything looked fine for the first three steps. I thought to myself, this is way easier than I expected. But I was wrong.

The agent hit an error, hallucinated a strange fix, and then tried to fix that by executing a command that nuked my workspace filesystem. Everything was corrupted, without warning. If you are building with LLMs right now, you already know that giving an agent raw shell access feels like giving the summer intern prod credentials on their first day. But the real problem isn't just that agents break things. It's that when they do, we don't have a standard way to hit undo.

The Missing Primitive

Right now, the standard answer to this problem would be: "Why don't you just use a sandbox?" But spinning up a temporary Docker container or microVM only solves one half of the problem: blast radius. It stops the agent from destroying your machine, but it does absolutely nothing to protect the internal state of the task itself.

Let's take a 10-step agent workflow, for example. If the agent does a flawless job on steps 1 through 5, but completely breaks the workspace on step 6, the entire run is ruined. Because the sandbox doesn't have instant state recovery built into the process, your only real option is to start the entire loop from scratch.

In standard software development, we have git checkout or VM snapshots to save our state. But when it comes to autonomous agents running terminal commands, we are completely missing a basic safety primitive: instant checkpointing.

The Lightbulb Moment

I realized we don't need heavier sandboxes; we just need a way to travel back in time. If you look at how agents run right now, it's a fragile linear setup:

[Step 1: OK] -> [Step 2: OK] -> [Step 3: rm -rf /] -> ๐Ÿ’ฅ TOTAL CRASH (Start Over)

Instead, I feel the workflow should look more like a Git tree. Before the agent executes any tool call or bash command, the system should automatically drop a silent checkpoint. If the command succeeds, the checkpoint merges. If the agent hallucinates a mistake, you instantly roll the entire filesystem back to the exact second before the command ran:

[Step 1: OK]
โ†“
[Step 2: OK] -> (Checkpoint Created)
โ†“
[Step 3: rm -rf /] -> ๐Ÿ’ฅ FAILED -> [๐Ÿ”„ ROLLBACK TO STEP 2] -> [Step 3 (Retry): OK]

But to make this practical, it can't be slow, because no one wants to wait 5 seconds to take a VM snapshot between every single tool call. It had to be a lightweight, near-instant filesystem trick.

How it Works

To make checkpointing near-instantaneous, I avoided heavy VM snapshots and went straight to the Linux kernel level using OverlayFS. OverlayFS lets you combine two directories-a read-only base layer and a blank writeable layer-into a single merged view.

When the AI agent runs a command, it thinks it is editing your real project files, but the OS is actually writing those changes to an isolated scratch space:

.rewind/
โ”œโ”€โ”€ base/      <-- Your original, untouched workspace files (Read-Only)
โ”œโ”€โ”€ scratch/   <-- Where the agent's changes actually go (Write-Only)
โ””โ”€โ”€ merged/    <-- What the agent actually sees and interacts with

Because of this copy-on-write mechanism, creating a checkpoint is a sub-20 ms operation. If the agent runs a successful command, you just commit the changes by syncing the scratch layer into your main workspace. But if the agent does something destructive, you just unmount the directory and spin up a blank one.

Here is the logic handling the cycle written out in TypeScript for brevity:

// abstraction of runtime loop
async function runAgentStep(agent, command) {
  // Checkpoint filesystem
  const checkpoint = await filesystem.createCheckpoint();
  try {
    // Let the agent execute the terminal tool
    await agent.execute(command);
    // if success, keep changes
    await checkpoint.commit();
  } catch (error) {
    // wipe scratch layer otherwise
    console.log("Agent hallucinated. Rolling back filesystem...");
    await checkpoint.rollback();
  }
}

Packaging it into rewind-sdk

I got tired of manually piecing together custom shell scripts and OverlayFS mount commands every time I wanted to test a new agent script. So, I packaged all of this underlying Linux filesystem logic into a lean, open-source infra layer called rewind-sdk.

The goal here was to make checkpoint and rollbacks completely framework-agnostic. It currently only supports LangGraph, raw scripts, and standard tool calls, but ideally the system built is designed to be extended to other agent frameworks.

The killer feature, however, is atomic state management: when you set a checkpoint or rollback, the SDK syncs the filesystem state and your conversation history together in a single call.

Here is how simple it is to initialize a protected runtime session and use it in Python:

from rewind_sdk import session, Verifier

# initialize the protected workspace environment
with session("agent_sandbox", workspace="./my_codebase", auto_commit=True) as sess:
    # set known-good checkpoint before risky actions
    sess.checkpoint("known_good")

    # configure automated rollback rules/triggers
    sess.auto_rollback(
        "exception",
        "test_failure",
        to="known_good",
        verifier=Verifier(command="pytest", retries=2, timeout=30.0),
    )

    # Simulating an agent step: modifying files inside the sandbox
    sess.write_file("auth.py", change_from_llm)

    try:
        # Run tests. If the verifier fails, auto_rollback fires instantly.
        sess.run_tests()
    except RuntimeError:
        print("Agent broke the build. Filesystem and memory rolled back!")

This is just the tip of the iceberg. With the @session.tool decorator, you can turn your tool functions into rollback-compatible actions. And with further verification features, you can hook your external, dedicated testing suite into rewind-sdk for more control over failure management.

This way, you get full, deterministic control over your workspace state. If the agent hallucinates a destructive command, the container discards the broken layer, wipes the dangling messages from the history so LLM providers don't reject the schema, and lets the agent try again from a clean slate.

Where to from here?

Building autonomous agents that actually do things is super exciting. However, we need better runtime guardrails if we want to move past simple chat interfaces. I'm currently treating this as an early prototype and building it entirely in public.

If you want to check out the full source code, try running the demo script, or even integrate it into your own agent workflows, everything is completely open-source:

  • Github Repo Link
  • PyPI

To get started:

pip install rewind-sdk

(Note: You'll need Docker running locally for the containerized isolation)

How are you currently handling agent safety and runtime errors in your own workflows? Let me know in the comments; I'd love to hear how others are tackling this problem.

Comments

No comments yet. Start the discussion.