Skip to content

[Bug] ZCODE 3.2.5 Edit tool drops file permissions (executable bit) on edit #87

Description

@golfromeo-fr

提交前确认 · Pre-submission checklist

  • 我已搜索过现有 issue,确认这不是重复 / I searched existing issues and confirmed this isn't a duplicate.
  • 我已阅读 CONTRIBUTING.md / I've read CONTRIBUTING.md.

问题类别 · Category

UI / 界面体验 · UI / UX

涉及的 Agent 框架 · Agent framework

ZCode Agent(自研)

严重程度 · Severity

影响体验 · Major (功能可用但体验受损 / works but degraded)

复现频率 · Reproducibility

必现 · Always

问题描述 · Description

Bug: Edit tool drops file permissions (executable bit) on edit

Task ID: sess_401e6b50-fd6b-411d-af7d-d006f1b3afa7
Workspace: /home/gr/supreme-mcp-tools

复现步骤 · Steps to reproduce

Issue

The Edit tool silently resets file permissions when modifying an existing file.
Editing a file with the executable bit (chmod +x, git mode 100755) produces
a non-executable file (100644) after the edit, with no warning. This breaks
executable scripts invoked directly via shebang.

Example (this task): I edited launchmcp.py (committed as 100755,
invoked as ./launchmcp.py from my startlauncher wrapper) and startlauncher
(also 100755). After the edits, both became non-executable, and
./startlauncher silently failed with "Permission denied". The user had to
diagnose and chmod +x by hand.

Reproduction:

$ echo '#!/bin/sh' > demo.sh && echo 'echo hi' >> demo.sh && chmod +x demo.sh
# edit demo.sh with ZCode Edit tool (any change), then:
$ ls -l demo.sh
-rw-r--r-- ... demo.sh   # exec bit GONE
$ git ls-files -s demo.sh
100644 ... demo.sh       # git sees mode change 100755 => 100644

Why it matters: Silent (no warning), breaks executable scripts, and the
regression can be committed to git permanently.

Likely root cause: The Edit tool appears to write content to a temp file
then atomically os.replace(tmp, target). I verified this mechanism reproduces
the symptom: os.replace replaces the target inode, so the target's existing
mode/owner/ACLs are lost — the file ends up with the temp file's mode. By
contrast, in-place rewrite (open(path,'w'), which truncates the existing
inode) preserves mode. Confirmed: os.replace drops 0o7550o600; open(w)
keeps 0o755.

Suggested fix: Preserve the target's mode across the atomic write:

import shutil
shutil.copystat(target, tmp)   # copy mode+xattrs to temp BEFORE replace
os.replace(tmp, target)

Affects any file whose mode differs from umask default (read-only 0o444,
group-writable, ACL-restricted), not just executable scripts.

Expected result

Editing a 100755 file should leave it 100755 — the executable bit (and all
file metadata) must survive edits. git diff --summary should show no
mode change after an edit, and the file should remain directly executable
with no manual chmod +x. If atomic temp+replace write is needed for crash
safety, call shutil.copystat(old, tmp) before the replace.

Env: Linux Debian 13, Python 3.13.5, umask 0002. Observed on two executable
scripts (launchmcp.py, startlauncher) committed as 100755, dropped to
100644/664 after one Edit each.

期望表现 · Expected behavior

When editing files, the file permissions should be kept as they are. Opencode, github copilot, codex and claude code do that.

实际表现 · Actual behavior

when editing files, file permissions are reset so an executable script is not longer executable (that's annoying)

ZCode 版本 · ZCode version

3.2.5

设备 / 系统 / 浏览器 · Device / OS / Browser

Linux

截图 / 录屏 / 日志 · Screenshots / Recordings / Logs

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions