# NousResearch/hermes-agent · Validate required args in file and todo tools

## Open now

- 1 issue, 2 PRs open on GitHub.
- #31791: Bug: read_file produces confusing error when called with empty/missing path
- PR #31801: fix(file_tools): validate read_file path before dispatch (#31791)
- PR #106948: fix(tools): reject read_file calls with missing or empty path

These issues and fixes address missing or empty argument validation across file tools (read_file, write_file) and the todo tool, where absent or empty inputs were silently accepted and produced corrupt or zero-byte output. The changes add upfront rejection of invalid arguments instead of silent substitution or misleading errors.

## TL;DR

- 12 member(s): 1 open issue(s), 2 closed issue(s), 2 open PR(s), 2 merged PR(s), 5 PR(s) closed without merging.
- A fix was merged to main in PR #19284. Verify it addresses your case.
- **Recommended:** read PR #19284 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-19284,pr-133864`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.15`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Open related issues

- #31791: Bug: read_file produces confusing error when called with empty/missing path

## Closed / duplicate issues

- #19096: write_file silently produces zero-byte files when content arg is missing
- #44496: todo_tool: silent substitution of '(no description)' for empty content corrupts todo list

## Prior & competing PRs

- **#19097: fix(tools): guard write_file against missing required args**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `e527240b2`, `#19096`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#19284: fix(tools): write_file rejects missing 'content'/'path' instead of creating zero-byte files**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `tests/tools/test_file_tools.py`, `tools/file_tools.py`
  - fix hunks:
  - `tests/tools/test_file_tools.py`

    ```diff
    @@ -104,6 +104,44 @@ def test_unexpected_exception_still_logs_error(self, mock_get, caplog):
             assert result["error"] == "boom"
             assert any("write_file error" in r.getMessage() for r in caplog.records)
     
    +    def test_missing_content_key_returns_error(self):
    +        """#19096 — handler must reject tool calls where 'content' key is absent."""
    +        from tools.file_tools import _handle_write_file
    +
    +        result = json.loads(_handle_write_file({"path": "/tmp/oops.md"}))
    +        assert "error" in result
    +        assert "content" in result["error"]
    +        assert "path" not in result.get("error", "").lower() or "missing" not in result.get("error", "").lower() or True  # just check error present
    +
    +    def test_missing_path_key_returns_error(self):
    +        """#19096 — handler must reject tool calls where 'path' key is absent."""
    +        from tools.file_tools import _handle_write_file
    +
    +        result = json.loads(_handle_write_file({"content": "hello"}))
    +        assert "error" in result
    +
    +    def test_explicit_empty_content_is_allowed(self):
    +        """#19096 — explicit empty string content (file truncation) must still work."""
    +        from tools.file_tools import _handle_write_file
    +
    +        with patch("tools.file_tools._get_file_ops") as mock_get:
    +            mock_ops = MagicMock()
    +            result_obj = MagicMock()
    +            result_obj.to_dict.return_value = {"status": "ok", "path": "/tmp/empty.txt", "bytes": 0}
    +            mock_ops.write_file.return_value = result_obj
    +            mock_get.return_value = mock_ops
    +
    +            result = json.loads(_handle_write_file({"path": "/tmp/empty.txt", "content": ""}))
    +            assert result["status"] == "ok"
    +
    +    def test_non_string_content_returns_error(self):
    +        """#19096 — content must be a string, not a dict or list."""
    +        from tools.file_tools import _handle_write_file
    +
    +        result = json.loads(_handle_write_file({"path": "/tmp/x.txt", "content": {"nested": "dict"}}))
    +        assert "error" in result
    +        assert "string" in result["error"].lower() or "content" in result["error"].lower()
    +
     
     class TestPatchHandler:
         @patch("tools.file_tools._get_file_ops")
    ```
  - `tools/file_tools.py`

    ```diff
    @@ -1097,7 +1097,25 @@ def _handle_read_file(args, **kw):
     
     def _handle_write_file(args, **kw):
         tid = kw.get("task_id") or "default"
    -    return write_file_tool(path=args.get("path", ""), content=args.get("content", ""), task_id=tid)
    +    if not args.get("path") or not isinstance(args.get("path"), str):
    +        return tool_error(
    +            "write_file: missing required field 'path'. Re-emit the tool call with "
    +            "both 'path' and 'content' set."
    +        )
    +    if "content" not in args:
    +        return tool_error(
    +            "write_file: missing required field 'content'. The tool call included a "
    +            "path but no content argument — this is almost always a dropped-arg bug "
    +            "under context pressure. Re-emit the tool call with the full content "
    +            "payload, or use execute_code with hermes_tools.write_file() for very "
    +            "large files."
    +        )
    +    if not isinstance(args["content"], str):
    +        return tool_error(
    +            f"write_file: 'content' must be a string, got "
    +            f"{type(args['content']).__name__}."
    +        )
    +    return write_file_tool(path=args["path"], content=args["content"], task_id=tid)
     
     
     def _handle_patch(args, **kw):
    ```
- **#133864: fix(todo): reject empty content instead of substituting '(no description)'**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `tests/tools/test_todo_tool.py`, `tools/todo_tool.py`
- **#19119: fix(tools): write_file rejects missing 'content'/'path' instead of creating zero-byte files (#19096)**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#19097`
- **#31801: fix(file_tools): validate read_file path before dispatch (#31791)**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `tests/tools/test_file_tools.py`, `tools/file_tools.py`
- **#31878: fix(file-tools): reject empty read_file path**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#31801`, `#31791`
- **#44498: fix(todo): reject empty content instead of substituting (no description) (#44496)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`
- **#83712: fix(todo): reject empty content instead of silent placeholder**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#44498`, `#44496`, `#44498`
- **#106948: fix(tools): reject read_file calls with missing or empty path**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#31801`, `#31791`
  - files: `tests/tools/test_file_tools.py`, `tools/file_tools.py`

## Fix status

- Merged to main in PR #19284 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-10-06`.
- Subsystem: `agent`
