{"repo":"NousResearch/hermes-agent","public_id":"4581","display_label":"read_file line numbers leaking into write_file content","summary":"Most members concern read_file output with line-number prefixes being written back through write_file (including in the execute_code sandbox), and propose options such as raw reads, rejecting, or stripping prefixes. One member about pipe-delimited data with a header row appears only loosely related.","subsystem":"agent","counts":{"members":11,"open_issues":0,"open_issues_distinct":0,"open_prs":4,"closed_issues":3,"merged_prs":1,"attempt_prs":7},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":50318,"merged_at":"2026-06-21T18:56:00+00:00"},"resolution":{"state":"partially_resolved","by":"pr-50318"},"freshness":{"state":"verified","last_verified_at":"2026-06-21T18:56:00+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.15854545454545454},"fix_edges":[{"issue_number":19798,"pr_number":50318,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-21T18:56:00+00:00"}],"members":[{"kind":"issue","number":4581,"title":"read_file should have option to return raw content without line numbers","url":null,"state":"closed"},{"kind":"issue","number":19798,"title":"read_file returns line-numbered content that pollutes files when written back via write_file","url":null,"state":"closed"},{"kind":"issue","number":46465,"title":"execute_code: read_file line numbers leak into write_file content","url":null,"state":"closed"},{"kind":"pr","number":19820,"title":"fix(file): reject read_file line-numbered writeback","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"71274f264","number":0},{"kind":"pr","value":"#50318","number":50318}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":28565,"title":"fix: reject line-numbered read_file output in write_file to prevent silent file corruption","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#50318","number":50318},{"kind":"pr","value":"#19820","number":19820},{"kind":"pr","value":"#19798","number":19798}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":42144,"title":"feat(tools): add add_line_numbers option to read_file","url":null,"state":"open","close_reason":{"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":46486,"title":"Fix raw read_file support in execute_code sandbox","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":46503,"title":"fix(tools): strip line-number prefixes in sandbox write_file to prevent read→write accumulation","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"71274f264","number":0}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":46505,"title":"fix(tools): expose raw sandbox file reads","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#46486","number":46486}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":50318,"title":"fix(file): reject read_file line-numbered output in write_file","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["tests/tools/test_file_read_guards.py","tests/tools/test_file_tools.py","tools/file_tools.py"],"fix_hunks":[{"file":"tests/tools/test_file_read_guards.py","hunks":["@@ -275,7 +275,7 @@ def test_write_rejects_internal_read_status_text(self, mock_ops):\n         ))\n \n         self.assertIn(\"error\", result)\n-        self.assertIn(\"internal read_file status text\", result[\"error\"])\n+        self.assertIn(\"internal read_file display text\", result[\"error\"])\n         fake.write_file.assert_not_called()\n \n     @patch(\"tools.file_tools._get_file_ops\")","@@ -299,7 +299,7 @@ def test_write_rejects_status_text_with_small_framing(self, mock_ops):\n         ))\n \n         self.assertIn(\"error\", result)\n-        self.assertIn(\"internal read_file status text\", result[\"error\"])\n+        self.assertIn(\"internal read_file display text\", result[\"error\"])\n         fake.write_file.assert_not_called()\n \n     @patch(\"tools.file_tools._get_file_ops\")"]},{"file":"tests/tools/test_file_tools.py","hunks":["@@ -91,6 +91,33 @@ def test_permission_error_returns_error_json_without_error_log(self, mock_get, c\n         assert any(\"write_file expected denial\" in r.getMessage() for r in caplog.records)\n         assert not any(r.levelno >= logging.ERROR for r in caplog.records)\n \n+    @patch(\"tools.file_tools._get_file_ops\")\n+    def test_rejects_read_file_line_numbered_content(self, mock_get):\n+        \"\"\"#19798 — do not persist read_file's LINE_NUM|CONTENT display format.\"\"\"\n+        from tools.file_tools import write_file_tool\n+\n+        content = \" 1|setting: new_value\\n 2|other: thing\\n\"\n+        result = json.loads(write_file_tool(\"/tmp/config.yaml\", content))\n+\n+        assert \"error\" in result\n+        assert \"line-number\" in result[\"error\"].lower()\n+        mock_get.assert_not_called()\n+\n+    @patch(\"tools.file_tools._get_file_ops\")\n+    def test_allows_sparse_literal_pipe_content(self, mock_get):\n+        \"\"\"A single literal N| line should not be treated as read_file output.\"\"\"\n+        mock_ops = MagicMock()\n+        result_obj = MagicMock()\n+        result_obj.to_dict.return_value = {\"status\": \"ok\", \"path\": \"/tmp/out.txt\", \"bytes\": 21}\n+        mock_ops.write_file.return_value = result_obj\n+        mock_get.return_value = mock_ops\n+\n+        from tools.file_tools import write_file_tool\n+        result = json.loads(write_file_tool(\"/tmp/out.txt\", \"1|literal value\\nplain line\\n\"))\n+\n+        assert result[\"status\"] == \"ok\"\n+        mock_ops.write_file.assert_called_once()\n+\n     @patch(\"tools.file_tools._get_file_ops\")\n     def test_unexpected_exception_still_logs_error(self, mock_get, caplog):\n         mock_get.side_effect = RuntimeError(\"boom\")"]},{"file":"tools/file_tools.py","hunks":["@@ -657,6 +657,49 @@ def _is_internal_file_status_text(content: str) -> bool:\n     return False\n \n \n+def _looks_like_read_file_line_numbered_content(content: str) -> bool:\n+    \"\"\"Return True for content dominated by read_file's ``LINE_NUM|CONTENT`` display.\n+\n+    ``read_file`` intentionally returns line-numbered text to the model. If\n+    that display format is echoed into ``write_file``, config/source files are\n+    silently corrupted with prefixes like `` 1|``.  We reject writes where the\n+    non-empty lines are mostly consecutive read_file-style numbered lines, while\n+    allowing sparse literal pipe content such as a single ``1|value`` line.\n+    \"\"\"\n+    if not isinstance(content, str):\n+        return False\n+\n+    lines = [line for line in content.splitlines() if line.strip()]\n+    if len(lines) < 2:\n+        return False\n+\n+    numbered: list[int] = []\n+    for line in lines:\n+        stripped = line.lstrip()\n+        prefix, sep, _rest = stripped.partition(\"|\")\n+        if sep and prefix.isdigit():\n+            numbered.append(int(prefix))\n+\n+    if len(numbered) < 2:\n+        return False\n+    if len(numbered) / len(lines) < 0.6:\n+        return False\n+\n+    consecutive_pairs = sum(\n+        1 for prev, current in zip(numbered, numbered[1:])\n+        if current == prev + 1\n+    )\n+    return consecutive_pairs >= len(numbered) - 1\n+\n+\n+def _is_internal_file_tool_content(content: str) -> bool:\n+    \"\"\"Return True when content is file-tool display text, not intended file bytes.\"\"\"\n+    return (\n+        _is_internal_file_status_text(content)\n+        or _looks_like_read_file_line_numbered_content(content)\n+    )\n+\n+\n def _get_file_ops(task_id: str = \"default\") -> ShellFileOperations:\n     \"\"\"Get or create ShellFileOperations for a terminal environment.\n ","@@ -1213,10 +1256,11 @@ def write_file_tool(path: str, content: str, task_id: str = \"default\",\n         cross_warning = _check_cross_profile_path(path, task_id)\n         if cross_warning:\n             return tool_error(cross_warning)\n-    if _is_internal_file_status_text(content):\n+    if _is_internal_file_tool_content(content):\n         return tool_error(\n-            \"Refusing to write internal read_file status text as file content. \"\n-            \"Re-read the file or reconstruct the intended file contents before writing.\"\n+            \"Refusing to write internal read_file display text as file content. \"\n+            \"Strip read_file line-number prefixes or reconstruct the intended \"\n+            \"file contents before writing.\"\n         )\n     try:\n         # Resolve once for the registry lock + stale check.  Failures here\n"]}]},{"kind":"pr","number":127457,"title":"fix(file): let write_file accept pipe-delimited data with a header row","url":null,"state":"open","close_reason":{"category":"silent_close","implication":"active"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}