From 11fb071788d169b1a8145bec3338de3197fef123 Mon Sep 17 00:00:00 2001 From: Jacob Stephens Date: Thu, 22 Oct 2026 07:56:43 -0400 Subject: [PATCH] Hide Claude Code attribution in factory sessions (#937) --- docs/guide.md | 2 ++ prompts/README.md | 4 +-- site/docs/index.html | 1 + site/prompts/index.html | 4 +-- src/harness/claude.rs | 66 ++++++++++++++++++++++++++++++++++++++--- src/prompts_page.rs | 41 ++++++++++++++++++++++++- 6 files changed, 109 insertions(+), 9 deletions(-) diff --git a/docs/guide.md b/docs/guide.md index cbbb1efad..791e6fc27 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -303,6 +303,8 @@ The user account that runs thirdshift needs: Sessions run headless with the full permissions of that user account, including `sudo` if the user has it, and nobody is there to approve anything. On Claude Code they run in auto mode (`claude -p --permission-mode auto`): its classifier checks each action and may block ones it judges risky, such as destructive commands or actions outside the task. A blocked action the agent can't work around can end the Run as a Failed run. The other Harnesses run with approvals and their sandbox turned off ([ADR-0013](adr/0013-every-harness-runs-unattended-and-fully-trusted.md)), so no approval step or sandbox stands between an action and the account. +Every factory session on Claude Code, Repairs, reviews and Resumes included, also passes `--settings '{"attribution":{"commit":"","pr":""}}'`. These per-session settings override the user's and repository's attribution settings, hiding Claude Code's commit trailer and pull request attribution. The pull request keeps thirdshift's `Built with` line as its only attribution; nothing replaces the commit trailer. Claude Code run by hand keeps its own settings. The release-summary invocation, which runs with tools disabled, keeps its existing arguments. + `claude -p` exits as soon as the agent ends its turn, killing any background task it started. So every prompt tells the agent to run long commands, such as tests, in the foreground, and to stop any background task it no longer needs, with the `TaskStop` tool, before it ends its turn. If a session still ends with background work running, thirdshift takes it as work the session was waiting on and gives it a **Resume**: it continues that same session once (`claude -p --resume`), asking the agent to re-run the work in the foreground and finish. If the Resume ends the same way, the work may have been abandoned, such as a hung test run the agent could not stop: thirdshift says which task was killed and carries on, so the pull request check, CI and the rest decide how the Run ends. If the Run then fails, its cause names the killed task ahead of what failed. ### What sessions leave behind diff --git a/prompts/README.md b/prompts/README.md index 59e3c7d90..ad2db4c7d 100644 --- a/prompts/README.md +++ b/prompts/README.md @@ -7,13 +7,13 @@ Every Session prompt thirdshift can start or resume an agent session with, one f Every session runs `claude` headless in the Run's worktree, in auto permission mode. The Factory skills are linked into the worktree's `.claude/skills/`, where Claude Code finds them, and kept out of git: ```sh -claude -p --permission-mode auto --output-format stream-json --verbose +claude -p --permission-mode auto --settings '{"attribution":{"commit":"","pr":""}}' --output-format stream-json --verbose ``` A [Resume](resume.md) continues the session that ended: ```sh -claude -p --permission-mode auto --output-format stream-json --verbose --resume +claude -p --permission-mode auto --settings '{"attribution":{"commit":"","pr":""}}' --output-format stream-json --verbose --resume ``` With `harness codex`, each session runs `codex exec --json --dangerously-bypass-approvals-and-sandbox -m -c model_reasoning_effort="" -c project_doc_fallback_filenames=["CLAUDE.md"] ` instead, without `-m ` or the Effort's `-c` where they are not set, and with `resume ` before the prompt for a Resume. The Factory skills are linked into `.agents/skills/`, and a prompt's first line loads its skill as `$thirdshift-`. diff --git a/site/docs/index.html b/site/docs/index.html index e660285c5..29bbc64c9 100644 --- a/site/docs/index.html +++ b/site/docs/index.html @@ -312,6 +312,7 @@

Unattended sessions

Sessions run headless with the full permissions of that user account, including sudo if the user has it, and nobody is there to approve anything. On Claude Code they run in auto mode (claude -p --permission-mode auto): its classifier checks each action and may block ones it judges risky, such as destructive commands or actions outside the task. A blocked action the agent can't work around can end the Run as a Failed run. The other Harnesses run with approvals and their sandbox turned off (ADR-0013), so no approval step or sandbox stands between an action and the account.

+

Every factory session on Claude Code, Repairs, reviews and Resumes included, also passes --settings '{"attribution":{"commit":"","pr":""}}'. These per-session settings override the user's and repository's attribution settings, hiding Claude Code's commit trailer and pull request attribution. The pull request keeps thirdshift's Built with line as its only attribution; nothing replaces the commit trailer. Claude Code run by hand keeps its own settings. The release-summary invocation, which runs with tools disabled, keeps its existing arguments.

claude -p exits as soon as the agent ends its turn, killing any background task it started. So every prompt tells the agent to run long commands, such as tests, in the foreground, and to stop any background task it no longer needs, with the TaskStop tool, before it ends its turn. If a session still ends with background work running, thirdshift takes it as work the session was waiting on and gives it a Resume: it continues that same session once (claude -p --resume), asking the agent to re-run the work in the foreground and finish. If the Resume ends the same way, the work may have been abandoned, such as a hung test run the agent could not stop: thirdshift says which task was killed and carries on, so the pull request check, CI and the rest decide how the Run ends. If the Run then fails, its cause names the killed task ahead of what failed.

What sessions leave behind

diff --git a/site/prompts/index.html b/site/prompts/index.html index a702a6394..30b4ce354 100644 --- a/site/prompts/index.html +++ b/site/prompts/index.html @@ -49,9 +49,9 @@

Prompts and skills

The command line

How each session starts

Every session runs claude headless in the Run's worktree, in auto permission mode. The Factory skills are linked into the worktree's .claude/skills/, where Claude Code finds them, and kept out of git. The prompt is one of the templates below.

-
claude -p --permission-mode auto --output-format stream-json --verbose <prompt>
+
claude -p --permission-mode auto --settings '{"attribution":{"commit":"","pr":""}}' --output-format stream-json --verbose <prompt>

A Resume continues the session that ended:

-
claude -p --permission-mode auto --output-format stream-json --verbose --resume <session id> <prompt>
+
claude -p --permission-mode auto --settings '{"attribution":{"commit":"","pr":""}}' --output-format stream-json --verbose --resume <session id> <prompt>

With harness codex, each session runs codex exec --json --dangerously-bypass-approvals-and-sandbox -m <model> -c model_reasoning_effort="<effort>" -c project_doc_fallback_filenames=["CLAUDE.md"] <prompt> instead, without -m <model> or the Effort's -c where they are not set, and with resume <session id> before the prompt for a Resume. The Factory skills are linked into .agents/skills/, and a prompt's first line loads its skill as $thirdshift-<skill>.

diff --git a/src/harness/claude.rs b/src/harness/claude.rs index 3d41f5b92..851dca55c 100644 --- a/src/harness/claude.rs +++ b/src/harness/claude.rs @@ -5,7 +5,7 @@ use std::process::Command; use anyhow::{Context, Result, bail}; -use super::adapter::{Adapter, Invocation, SkillLoading}; +use super::adapter::{Adapter, Invocation, SkillLoading, without}; use super::interpretation::{Interpretation, Stream}; use super::settings::{Terminal, ask_setting}; use super::{Choice, Harness, ModelAndEffort, Settings, said}; @@ -14,6 +14,8 @@ use crate::progress; pub struct Claude; pub const REGISTRATION: (Harness, &dyn Adapter) = (Harness::Claude, &Claude); +const ATTRIBUTION_SETTINGS: &str = r#"{"attribution":{"commit":"","pr":""}}"#; + impl Adapter for Claude { fn name(&self) -> &'static str { "claude" @@ -51,6 +53,7 @@ impl Adapter for Claude { } fn summary(&self, choice: &Choice, prompt: &str, _prompt_file: &Path) -> Invocation { let mut invocation = self.session(choice, None, prompt); + invocation.args = without(invocation.args, &["--settings", ATTRIBUTION_SETTINGS]); invocation.args.pop(); invocation.stdin = Some(prompt.to_string()); invocation.args.splice( @@ -85,14 +88,21 @@ pub fn model_args(choice: &Choice) -> Vec<&str> { } /// The arguments every session runs `claude` with: headless in auto mode, -/// on the Model and Effort `harness` sets, if any, streaming JSON. With -/// `resume`, the session with that id continues. The prompt comes last. +/// on the Model and Effort `harness` sets, if any, streaming JSON, with +/// Claude Code's commit and PR attribution hidden. With `resume`, the session +/// with that id continues. The prompt comes last. pub fn claude_args<'a>( harness: &'a Choice, resume: Option<&'a str>, prompt: &'a str, ) -> Vec<&'a str> { - let mut args = vec!["-p", "--permission-mode", "auto"]; + let mut args = vec![ + "-p", + "--permission-mode", + "auto", + "--settings", + ATTRIBUTION_SETTINGS, + ]; args.extend(model_args(harness)); args.extend(["--output-format", "stream-json", "--verbose"]); if let Some(session_id) = resume { @@ -165,3 +175,51 @@ fn ask_claude(outside: &mut dyn Terminal, current: &ModelAndEffort) -> Result String { /// resumes with, with the page's placeholders. fn command(resume: Option<&str>) -> String { let harness = Choice::default(); - format!("claude {}", claude_args(&harness, resume, PROMPT).join(" ")) + let args: Vec<_> = claude_args(&harness, resume, PROMPT) + .into_iter() + .map(|arg| { + // Keep inline settings JSON intact when copied into a shell. + if arg.starts_with('{') { + format!("'{arg}'") + } else { + arg.to_string() + } + }) + .collect(); + format!("claude {}", args.join(" ")) } /// The `codex` command line a session on a Model and Effort starts with, @@ -640,6 +651,34 @@ mod tests { std::env::var_os("UPDATE_PROMPTS").is_some() } + #[test] + fn prompts_page_claude_commands_preserve_attribution_settings_as_one_shell_argument() { + for resume in [None, Some(SESSION_ID)] { + let command = command(resume) + .replace(PROMPT, "prompt") + .replace(SESSION_ID, "session-id"); + let output = std::process::Command::new("bash") + .args([ + "-c", + &format!("claude() {{ printf '%s\\n' \"$@\"; }}; {command}"), + ]) + .output() + .unwrap(); + assert!(output.status.success(), "{output:?}"); + let stdout = String::from_utf8(output.stdout).unwrap(); + let args: Vec<_> = stdout.lines().collect(); + let settings = args + .windows(2) + .find(|pair| pair[0] == "--settings") + .expect("the documented command must pass attribution settings"); + let settings: serde_json::Value = serde_json::from_str(settings[1]).unwrap(); + assert_eq!( + settings, + serde_json::json!({"attribution": {"commit": "", "pr": ""}}) + ); + } + } + /// The golden-file test: the page carries the fragment as rendered now. /// The crates.io package leaves `site/` out, so it skips there. #[test]