Skip to content

Commit be4df30

Browse files
authored
Merge pull request #5 from mtsfoni/copilot/remove-watermark-from-commits
Remove "Generated by construct" watermark from git commits
2 parents f3863d0 + d7f7197 commit be4df30

7 files changed

Lines changed: 12 additions & 95 deletions

File tree

CHANGELOG.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,6 @@
7575
the committer falls back to the author (matching git's default behaviour). The
7676
synthetic fallback (`construct user <user@construct.local>`) is only used when
7777
no author identity at all is available on the host, and triggers a warning.
78-
- A `commit-msg` hook is injected into the container at startup via
79-
`core.hooksPath`. It appends a `Generated by construct` git trailer to every
80-
commit message (idempotent — safe with amend and rebase).
8178

8279
---
8380

docs/spec/construct-agents-md.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ not override a project-level `AGENTS.md` or `CLAUDE.md` in the workspace.
9090

9191
| File | Change |
9292
|---|---|
93-
| `internal/runner/runner.go` | `CONSTRUCT=1` always injected; `generatedEntrypoint()` always writes `~/.config/opencode/AGENTS.md`, appending port rules only when `CONSTRUCT_PORTS` is set; also writes git identity hook and sets `core.hooksPath` (see `docs/spec/git-identity.md`) |
93+
| `internal/runner/runner.go` | `CONSTRUCT=1` always injected; `generatedEntrypoint()` always writes `~/.config/opencode/AGENTS.md`, appending port rules only when `CONSTRUCT_PORTS` is set |
9494
| `internal/runner/runner_test.go` | Tests updated to reflect always-present `CONSTRUCT=1` and always-present `AGENTS.md` |
9595
| `docs/spec/construct-agents-md.md` | This document |
9696

docs/spec/git-identity.md

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ attribution that comes from the developer's own git identity.
1010
## Solution
1111

1212
construct reads the host user's git identity at launch and injects it into the
13-
container. Every commit the agent makes is attributed to the real developer. A
14-
`commit-msg` hook appended to every commit message makes it clear the commit was
15-
generated by construct.
13+
container. Every commit the agent makes is attributed to the real developer.
1614

1715
## Behaviour
1816

@@ -60,37 +58,7 @@ GIT_COMMITTER_EMAIL=<resolved>
6058
The previous hard-coded `construct agent / agent@construct.local` identity is
6159
removed entirely.
6260

63-
### 3. Commit-msg hook
64-
65-
`generatedEntrypoint()` writes a `commit-msg` hook into the container at
66-
startup:
67-
68-
```
69-
/home/agent/.githooks/commit-msg
70-
```
71-
72-
and configures git to use it globally:
73-
74-
```sh
75-
git config --global core.hooksPath /home/agent/.githooks
76-
```
77-
78-
The hook appends a `Generated by construct` trailer to every commit message,
79-
following the git trailer convention (a blank line separates the body from the
80-
trailer block). The hook is idempotent: it checks for the presence of the
81-
trailer before appending, so amend and rebase do not produce duplicates.
82-
83-
Resulting commit message format:
84-
85-
```
86-
feat: add user login
87-
88-
Implement OAuth2 flow with token refresh.
89-
90-
Generated by construct
91-
```
92-
93-
### 4. Scope
61+
### 3. Scope
9462

9563
This is a runner-level change. It applies to all tools.
9664

@@ -104,7 +72,7 @@ No files are written to the host or to the workspace repo.
10472

10573
| File | Change |
10674
|---|---|
107-
| `internal/runner/runner.go` | `hostGitIdentity()`: resolves author/committer separately, honouring host env vars with committer falling back to author; `buildRunArgs`: injects resolved values; `generatedEntrypoint()`: writes `/home/agent/.githooks/commit-msg` and sets `git config --global core.hooksPath` |
75+
| `internal/runner/runner.go` | `hostGitIdentity()`: resolves author/committer separately, honouring host env vars with committer falling back to author; `buildRunArgs`: injects resolved values |
10876
| `docs/spec/git-identity.md` | This document |
10977
| `CHANGELOG.md` | Entry under `## [Unreleased]` |
11078

docs/spec/image-build-layers.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,7 @@ off to the tool:
8282
`~/.config/opencode/opencode.json` with the playwright MCP server entry.
8383
Otherwise deletes the file, preventing stale config from persisting across
8484
runs on the same home volume.
85-
3. **Git identity** — sets `git config --global core.hooksPath
86-
/home/agent/.githooks` and writes a `commit-msg` hook that appends a
87-
`Generated by construct` trailer to every commit message (idempotent).
88-
The host user's git identity is supplied via `GIT_AUTHOR_*` /
85+
3. **Git identity** — the host user's git identity is supplied via `GIT_AUTHOR_*` /
8986
`GIT_COMMITTER_*` env vars set in `buildRunArgs` (see
9087
`docs/spec/git-identity.md`).
9188
4. **Exec** — hands off to the tool command (`exec "$@"`).
@@ -109,7 +106,6 @@ Examples: `construct-ui-opencode`, `construct-go-opencode`.
109106
| AI tool binary (`npm install -g opencode-ai`) | Tool |
110107
| Entrypoint / secrets handling | Tool |
111108
| MCP config write/delete logic | Tool (entrypoint) |
112-
| Git identity hook (`commit-msg`, `core.hooksPath`) | Tool (entrypoint) |
113109
| `DOCKER_HOST` env var (runtime, not baked in) | Container run args |
114110
| Git author/committer identity (host-read at launch) | Container run args |
115111
| Workspace bind-mount | Container run args |

docs/threat-model.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ The agent does **not** have access to:
179179
| **Mitigation** | No git credential helper is wired up by construct. The agent would have to discover the token from its environment and construct the remote URL manually (e.g. `https://x-access-token:$TOKEN@github.com/...`). This is non-trivial but not difficult for a capable model. SSH keys are **not** mounted into the container, so SSH-based remotes are unreachable. |
180180
| **Residual risk** | No token with git push scope is injected by default. If a user supplies a GitHub token via `config set`, the agent could use it for HTTPS git operations. |
181181
| **Recommendation** | Use a token scoped to the minimum required permissions. A GitHub fine-grained PAT with read-only or repo-specific access limits blast radius. Avoid using tokens that have push access to repositories beyond the one you are actively working on. Note: recommending SSH remotes is **not** a useful mitigation here — SSH keys are not mounted, so the attack surface is HTTPS+token only. |
182-
| **Attribution note** | construct injects the host user's real git identity (`user.name` / `user.email`) as `GIT_AUTHOR_*` and `GIT_COMMITTER_*`. Any commits the agent makes — including any it pushes — will carry the developer's real name and email, plus a `Generated by construct` trailer. Users should be aware that agent-authored commits are attributable to them in git history. |
182+
| **Attribution note** | construct injects the host user's real git identity (`user.name` / `user.email`) as `GIT_AUTHOR_*` and `GIT_COMMITTER_*`. Any commits the agent makes — including any it pushes — will carry the developer's real name and email. Users should be aware that agent-authored commits are attributable to them in git history. |
183183
| **vs. host baseline** | On the host, the agent has access to the user's full git credential store (`~/.gitconfig`, credential helper, SSH keys, `~/.netrc`), giving push access to every repository the user can reach. construct limits this to explicitly injected tokens only. |
184184

185185
---

internal/runner/runner.go

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,7 @@ func buildRunArgs(cfg *Config, dindInst *dind.Instance, image, sessionID, homeVo
280280
// Git identity — use the host user's real identity so commits are attributed
281281
// to the person who ran construct. Author and committer are read separately
282282
// so that a host with distinct GIT_AUTHOR_* / GIT_COMMITTER_* env vars is
283-
// faithfully mirrored. The commit-msg hook (written by the entrypoint)
284-
// appends a "Generated by construct" trailer to every message.
283+
// faithfully mirrored.
285284
"-e", "GIT_AUTHOR_NAME="+authorName,
286285
"-e", "GIT_AUTHOR_EMAIL="+authorEmail,
287286
"-e", "GIT_COMMITTER_NAME="+committerName,
@@ -936,19 +935,6 @@ func generatedEntrypoint() string {
936935
"The host connects to it via http://localhost:${CONSTRUCT_SERVE_PORT}.\n" +
937936
"AGENTSEOF\n" +
938937
"fi\n" +
939-
"# Set up a global commit-msg hook that appends a 'Generated by construct'\n" +
940-
"# git trailer to every commit message. The hook is idempotent: it checks for\n" +
941-
"# the trailer before appending so amend/rebase does not produce duplicates.\n" +
942-
"mkdir -p \"${HOME}/.githooks\"\n" +
943-
"cat > \"${HOME}/.githooks/commit-msg\" << 'HOOKEOF'\n" +
944-
"#!/bin/sh\n" +
945-
"trailer='Generated by construct'\n" +
946-
"if ! grep -qF \"$trailer\" \"$1\"; then\n" +
947-
" printf '\\n%s\\n' \"$trailer\" >> \"$1\"\n" +
948-
"fi\n" +
949-
"HOOKEOF\n" +
950-
"chmod +x \"${HOME}/.githooks/commit-msg\"\n" +
951-
"git config --global core.hooksPath \"${HOME}/.githooks\"\n" +
952938
"exec \"$@\"\n"
953939
}
954940

internal/runner/runner_test.go

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1609,42 +1609,12 @@ func TestHostGitIdentity_CommitterFallsBackToAuthor(t *testing.T) {
16091609
}
16101610
}
16111611

1612-
// TestGeneratedEntrypoint_ContainsCommitMsgHook verifies that the generated
1613-
// entrypoint script sets up the commit-msg hook and configures core.hooksPath.
1614-
func TestGeneratedEntrypoint_ContainsCommitMsgHook(t *testing.T) {
1612+
// TestGeneratedEntrypoint_EndsWithExec verifies that the generated entrypoint
1613+
// script ends with exec "$@".
1614+
func TestGeneratedEntrypoint_EndsWithExec(t *testing.T) {
16151615
script := generatedEntrypoint()
1616-
checks := []struct {
1617-
desc string
1618-
snippet string
1619-
}{
1620-
{"creates .githooks dir", ".githooks"},
1621-
{"writes commit-msg hook file", "commit-msg"},
1622-
{"hook appends Generated by construct trailer", "Generated by construct"},
1623-
{"hook is idempotent (grep check)", "grep -qF"},
1624-
{"sets core.hooksPath globally", "core.hooksPath"},
1625-
{"hook is executable", "chmod +x"},
1626-
}
1627-
for _, c := range checks {
1628-
if !strings.Contains(script, c.snippet) {
1629-
t.Errorf("entrypoint: expected %s (snippet %q not found)", c.desc, c.snippet)
1630-
}
1631-
}
1632-
}
1633-
1634-
// TestGeneratedEntrypoint_HookBeforeExec verifies the commit-msg hook setup
1635-
// appears before the final exec line.
1636-
func TestGeneratedEntrypoint_HookBeforeExec(t *testing.T) {
1637-
script := generatedEntrypoint()
1638-
hookIdx := strings.Index(script, "commit-msg")
1639-
execIdx := strings.LastIndex(script, `exec "$@"`)
1640-
if hookIdx == -1 {
1641-
t.Fatal("commit-msg hook block not found in entrypoint")
1642-
}
1643-
if execIdx == -1 {
1644-
t.Fatal(`exec "$@" not found in entrypoint`)
1645-
}
1646-
if hookIdx > execIdx {
1647-
t.Error("commit-msg hook block appears after exec line; it must come before")
1616+
if !strings.Contains(script, `exec "$@"`) {
1617+
t.Error(`entrypoint: expected exec "$@" line not found`)
16481618
}
16491619
}
16501620

0 commit comments

Comments
 (0)