Repository navigation
Add file mode (executable bit) support to push_files #2578
Description
Feature request: expose file mode (executable bit) on push_files
Use case
Agentic coding harnesses use this MCP server to publish files into a repo
on behalf of an LLM (issue execution, autonomous PR workflows, etc.).
When the work the agent is asked to produce is a shell script, build
helper, or other executable, the resulting file needs the executable
bit set (100755) — otherwise reviewers either have to chmod +x after
merge or merge a broken artifact.
Today there is no way for an MCP client to land an executable file
through this server. Both file-write tools use APIs that surface a fixed
mode:
create_or_update_file→ Contents API (PUT /repos/.../contents/{path}),
which always writes mode100644. This is a GitHub API limitation, not
something this server can route around.push_files→ Git Data API (CreateTree+CreateCommit+
UpdateRef). The underlying API does support per-entry mode, but
the tool's input schema doesn't expose it. Each entry is currently
{content, path}only.
We've observed agents react to this in two unhelpful ways:
- Quiet hallucination. The agent reports in the PR body that it
pushed with mode100755when in fact the blob landed as100644. - Workaround scope drift. The agent invents a second file (e.g.
scripts/permissions.shcontainingfind scripts/ -exec chmod +x)
that wasn't part of the task, hoping a CI step or human will run it.
A first-class mode parameter on push_files would close both failure
modes cleanly.
Proposed change
Extend the push_files.files[] input schema with an optional mode
field, validated against the set the Git Data API accepts:
| Mode | Meaning |
|---|---|
100644 |
regular file (default if omitted) |
100755 |
executable file |
120000 |
symlink |
040000 |
subdirectory (subtree) |
160000 |
submodule |
For most agentic use cases only 100644 (default) and 100755 matter,
but echoing the full Git Data API set keeps parity with the underlying
endpoint.
Backwards compatible: existing callers that omit mode get 100644,
which is what they get today.
Implementation pointer
The relevant code is already calling Git.CreateTree with a
[]*github.TreeEntry (in pkg/github/repositories.go, around the
PushFiles tool definition). The go-github TreeEntry struct
already has a Mode *string field — it just isn't being set from the
tool input. Threading it through should be a small change:
// in the loop that builds entries:
mode := "100644"
if f.Mode != nil {
mode = *f.Mode
}
entries = append(entries, &github.TreeEntry{
Path: github.String(f.Path),
Mode: github.String(mode),
Type: github.String("blob"),
Content: github.String(f.Content),
})Plus input-schema additions for the optional field with the validated
set above.
Single-file case
create_or_update_file can't be fixed the same way because the Contents
API doesn't support mode at all — it's a GitHub API constraint, not a
server constraint. For the single-file executable case, callers can use
push_files with a one-entry files array. Worth noting that in the
tool's description so clients pick the right call.
Out of scope
- Changes to
create_or_update_file(blocked by Contents API). - Symlink/submodule write flows beyond just accepting their modes on the
schema. - Validating the executable-bit semantics on the receiving end.
Context
Filed against this server because we've been hitting the limitation
from a Claude-Code-plans / Goose-executes harness
(why-pengo/claude_and_goose),
where local-model executors are asked to ship small bash helpers as
part of issue-driven PRs. Happy to send a PR if the proposed shape
looks right.
Activity
nateberkopec commentedon Oct 4, 2026
It would be even better if it could preserve the existing mode automatically.
fix: preserve executable modes and support explicit regular-file modes
test: cover mode preservation and fail-closed tree lookup
fix: bound push_files tree traversal to 64 components