A mini code agent with Docker Agent - Part 2: `filesystem` + `todo`

6 min read

In the previous blog post, the agent only had shell. It could already read and write files (cat, echo >, sed...), but in a fragile and dangerous way: shell strings to escape, no limit on the paths touched, output to parse by hand.

A code agent deserves better. Today, this second blog post adds two built-in tools:

  • **filesystem**: typed and scoped file operations (read, write, list, search, edit);
  • **todo**: a task list the agent keeps itself so it doesn't get lost in a multi-step job.

The filesystem toolset

yaml
toolsets:
  - type: filesystem
    allow_list:
      - "."

Where shell exposes a generic command, filesystem exposes several dedicated tools to the model — typically read_file, write_file, list_directory, search, edit_file. Each has a clear schema (path, content...), so the model produces structured arguments instead of shell lines to quote.

In the loop, this gives clean calls, easy to read and to check:

text
Calling write_file(path: "notes.md", content: "# Mes notes")
write_file response → "File written successfully: notes.md (11 bytes)"
Calling read_file(path: "notes.md")
read_file response → "# Mes notes"

Scoping access: allow_list / deny_list

By default, the toolset is unrestricted: relative paths start from the working directory, but nothing stops the agent from reading/writing an absolute path or going up with ... Handy locally, dangerous as soon as an agent acts on someone else's behalf.

So we bound the access:

SettingEffect
allow_listlimits operations to a fixed set of roots
deny_listcarves out forbidden subtrees, even under an allowed root

Recognized tokens: "." (working directory), "~" (home), "~/foo", "${env.VAR}", an absolute path (as-is), a relative path (joined to the working directory). Symbolic links are resolved before the check: impossible to escape an allowed root through a symlink.

Here, allow_list: ["."] locks the agent inside its working directory.

filesystem scopes where the agent can go. In the next blog post we'll see how the permissions block scopes which actions (which tool calls) are allowed — two complementary safeguards.

A very, very, very important note: the safest way to use an agent without risk is to "contain" it in a restricted VM, a container with limited rights and network access, or a sandbox built specifically for this like **[sbx](https://docs.docker.com/ai/sandboxes/)**. If you use a VM or a container, "in theory" you limit access to the host filesystem, but you'll have to deal with network access, container security, and also the visibility of your secrets (environment variables, config files...); don't forget that the agent "sees everything" and can exfiltrate data through the network or the logs. When I'm back from holidays, I'll write an article about managing secrets with sbx.

The todo toolset

yaml
toolsets:
  - type: todo

There is nothing to configure. The tool gives the agent a task scratchpad: it adds the steps it plans to follow, then checks them off as it goes.

Why it's especially useful for a small model: the context window is narrow, and on a long task the model easily "forgets" where it is. An explicit task list, re-read at every turn, acts as a structured working memory: it keeps the course ("I still have to write the tests"), avoids omissions and repetitions.

We explicitly instruct it to use it:

yaml
instruction: |
  For any multi-step task, keep a list up to date with the `todo` tool:
  add the steps and mark them done as you go.

Note: keep in mind that we're working with a very small model, so a task list that's too long or too detailed can overflow the context window. So keep the list short and concise.

shell vs filesystem: which one, when?

shellfilesystem
Surfacehuge (the whole system)dedicated file operations
Securityarbitrary commandsbounded paths (allow_list/deny_list)
Outputtext to parsestructured results
Ideal forexploring, running tools (git, go test)reading/writing/editing files cleanly

The two are complementary: many code agents declare shell and filesystem. We split them here for teaching purposes; nothing stops you from combining the toolsets.

Our complete agent.yaml for this lesson:

yaml
agents:
  root:
    model: mellum
    description: A code agent that reads, writes and edits files, and tracks its work.
    instruction: |
      You are a code agent. Use the `filesystem` tools to read, create and edit
      files instead of printing code into the conversation. For any multi-step
      task, keep a list up to date with the `todo` tool: add the steps and mark
      them done as you go.
    toolsets:
      - type: filesystem
        allow_list:
          - "."
      - type: todo

models:
  mellum:
    provider: dmr
    model: huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M
    base_url: http://host.docker.internal:12434/engines/v1
    # base_url: http://localhost:12434/engines/v1

📝 You'll find all the configuration files over here: 02-filesystem-todo

Running it

We move into the lesson folder; the agent reads and writes in this folder (allow_list: ["."]):

bash
cd 02-filesystem-todo

# TUI
docker-agent run localhost.agent.yaml

# Or TUI with sbx (sandbox):
sbx run docker-agent --kit . -- run agent.yaml
tui-01
tui-01
tui-02
tui-02

You can also run the agent in a single command (without the TUI):

text
docker agent run --exec --yolo agent.yaml \
  "Create a notes.md file with the title '# Mes notes' then re-read it."

A complete example in the TUI

The best way to see the todo at work is to launch the TUI and hand it a task that clearly has several steps. Open the TUI:

bash
cd 02-filesystem-todo
docker-agent run localhost.agent.yaml

Then paste this prompt:

text
Create a small Node.js module `calc/` with its tests. Keep your todo up to date.

1. Create the folder `calc/`.
2. Write `calc/operations.js` with two exported functions: `add(a, b)` and `divide(a, b)`.
   `divide` must throw an `Error` when `b === 0`. Comment each function.
3. Write `calc/operations.test.js` with tests using Node's built-in runner
   (`node:test` and `node:assert`): one nominal case per function, plus a test
   asserting that `divide(1, 0)` throws.
4. Write `calc/README.md` explaining the module and how to run the tests (`node --test`).
5. Re-read `calc/operations.js` and `calc/operations.test.js` to check they are consistent.

When you are done, give me a recap of your todo.

Here the todo keeps the course on a classic chain code → tests → doc → review:

tui-03
tui-03

👋 Heads-up: if you want the agent to actually run the code, you'll need to add the shell tool to the toolsets section:

yaml
    toolsets:
      - type: filesystem
        allow_list:
          - "."
      - type: todo
tui-04
tui-04
tui-05
tui-05

Key takeaways

  • filesystem replaces shell hacks with typed and safe file operations.
  • allow_list/deny_list lock the agent inside a perimeter — essential outside the dev machine.
  • todo gives the model an explicit working memory, precious when the context is small.
  • You combine toolsets: an agent can have shell, filesystem and todo all at once.

See you very soon for the next part: the next blog post will add think and memory, so the agent can think before acting and remember what it did.

Written by

0 Comments

No comments yet. Be the first to comment!

Copyright © 2026k33g_org's BlogPowered by Writizzy