langchain-ai/deepagents · error · ValueError

allow_fs_tools must be None or a non-empty list

Error message

allow_fs_tools must be None or a non-empty list

What it means

`ServerConfig.__post_init__` treats `allow_fs_tools` as a security control: `None` means unrestricted, but an explicit list must be usable as an allowlist — non-empty and containing `read_file` (so the agent can always read files). This `ValueError` fires for an explicitly empty list; the sibling check raises for a list missing `read_file`. It is enforced at the single authoritative construction point so violations surface immediately, not a process boundary away in `FilesystemMiddleware`.

Source

Thrown at libs/code/deepagents_code/_server_config.py:494

                `allow_fs_tools` is an empty list or omits `"read_file"`, or
                `rubric_max_iterations` / `recursion_limit` is non-positive.
        """
        if self.sandbox_type == "none":
            object.__setattr__(self, "sandbox_type", None)
        if self.shell_allow_list is not None and len(self.shell_allow_list) == 0:
            msg = "shell_allow_list must be None or non-empty"
            raise ValueError(msg)
        # `allow_fs_tools` is a security control: `None` means unrestricted, but
        # an explicit list must be a usable allowlist. Own the non-empty +
        # `read_file`-required invariant here (the single authoritative point
        # for both the env round-trip via `from_env` and direct construction)
        # rather than deferring to `FilesystemMiddleware`, which would only
        # surface the violation a process boundary away. `_parse_allow_fs_tools_flag`
        # still enforces the same rule at the CLI for a friendlier error.
        if self.allow_fs_tools is not None:
            if len(self.allow_fs_tools) == 0:
                msg = "allow_fs_tools must be None or a non-empty list"
                raise ValueError(msg)
            if "read_file" not in self.allow_fs_tools:
                msg = "allow_fs_tools must include 'read_file'"
                raise ValueError(msg)
        if isinstance(self.rubric_max_iterations, bool):
            msg = "rubric_max_iterations must be None or a positive integer"
            raise TypeError(msg)
        if self.rubric_max_iterations is not None and self.rubric_max_iterations <= 0:
            msg = "rubric_max_iterations must be None or a positive integer"
            raise ValueError(msg)
        if isinstance(self.cli_max_retries, bool):
            msg = "cli_max_retries must be None or a non-negative integer"
            raise TypeError(msg)
        if self.cli_max_retries is not None and self.cli_max_retries < 0:
            msg = "cli_max_retries must be None or a non-negative integer"
            raise ValueError(msg)
        if isinstance(self.recursion_limit, bool):
            msg = "recursion_limit must be None or a positive integer"
            raise TypeError(msg)

View on GitHub (pinned to a1af029e6e)

Solutions

  1. Pass `allow_fs_tools=None` to mean unrestricted filesystem tools
  2. Provide a non-empty list that includes "read_file", e.g. allow_fs_tools=["read_file","write_file"]
  3. Fix upstream code so an emptied list becomes `None` rather than `[]`
  4. Use the `--allow-fs-tools` CLI flag, which enforces the same rule earlier with a friendlier error (`_parse_allow_fs_tools_flag`)

Example fix

// before
config = ServerConfig(allow_fs_tools=[], ...)
# ValueError: allow_fs_tools must be None or a non-empty list
// after
config = ServerConfig(allow_fs_tools=None, ...)  # or ["read_file", "glob"]
Defensive patterns

Strategy: validation

Validate before calling

def normalize_allow_fs_tools(tools: list[str] | None) -> list[str] | None:
    if not tools:
        return None  # empty -> unrestricted, avoids the empty-list error
    if "read_file" not in tools:
        tools = ["read_file", *tools]  # read_file is mandatory in any allowlist
    return tools

Type guard

def is_valid_allow_fs_tools(value: object) -> bool:
    if value is None:
        return True
    return (
        isinstance(value, list)
        and len(value) > 0
        and all(isinstance(t, str) for t in value)
        and "read_file" in value
    )

Try / catch

try:
    config = ServerConfig(**kwargs)
except ValueError as e:
    if "allow_fs_tools" in str(e):
        print(f"Invalid allow_fs_tools: {e}; pass None or a non-empty list including 'read_file'")
        sys.exit(2)
    raise

Prevention

When it happens

Trigger: Constructing `ServerConfig` with `allow_fs_tools=[]`, or setting `DEEPAGENTS_CODE_SERVER_ALLOW_FS_TOOLS` to a JSON array that round-trips to an empty list, then calling `from_env`/`from_cli_args`.

Common situations: Stripping all tools from a list programmatically and passing the result; an env deploy template rendering an empty array `[]`; code intending 'no restriction' that passes `[]` instead of `None`.

Related errors


AI-assisted analysis of langchain-ai/deepagents@a1af029e6e (2026-08-29). Data as JSON: /api/errors/2e47159aef4e23b7. Report an issue: GitHub.