on main help sign in
feat: add attribution settings to config and bash tool (#1025)

* feat: add attribution settings to config and bash tool * fix(readme): move ephemeral data block back to cfg section Closes: #445

commit6c380d4414ee83b49d04aa299ef0616b45408f26 authorAmolith <amolith@secluded.site> dateWed 17 Sep 2025, 11:29 (1 year ago) parentb00ffcc
5 files changed+123−26
README.md+33 internal/config/config.go+14−8 internal/llm/agent/agent.go+1−1 internal/llm/tools/bash.go+55−17 schema.json+20
README.md+33
@@ -173,6 +173,39 @@ $HOME/.local/share/crush/crush.json
173173%LOCALAPPDATA%\crush\crush.json
174174```
175175
176+### Attribution Settings
177+
178+By default, Crush adds attribution information to git commits and pull requests it creates. You can customize this behavior with the `attribution` option:
179+
180+```json
181+{
182+ "$schema": "https://charm.land/crush.json",
183+ "options": {
184+ "attribution": {
185+ "co_authored_by": true,
186+ "generated_with": true
187+ }
188+ }
189+}
190+```
191+
192+- `co_authored_by`: When true (default), adds `Co-Authored-By: Crush <crush@charm.land>` to commit messages
193+- `generated_with`: When true (default), adds `💘 Generated with Crush` line to commit messages and PR descriptions
194+
195+To disable all attribution, set both options to false:
196+
197+```json
198+{
199+ "$schema": "https://charm.land/crush.json",
200+ "options": {
201+ "attribution": {
202+ "co_authored_by": false,
203+ "generated_with": false
204+ }
205+ }
206+}
207+```
208+
176209### LSPs
177210
178211Crush can use LSPs for additional context to help inform its decisions, just
internal/config/config.go+14−8
@@ -138,15 +138,21 @@ type Permissions struct {
138138 SkipRequests bool `json:"-"` // Automatically accept all permissions (YOLO mode)
139139}
140140
141+type Attribution struct {
142+ CoAuthoredBy bool `json:"co_authored_by,omitempty" jsonschema:"description=Add Co-Authored-By trailer to commit messages,default=true"`
143+ GeneratedWith bool `json:"generated_with,omitempty" jsonschema:"description=Add Generated with Crush line to commit messages and issues and PRs,default=true"`
144+}
145+
141146type Options struct {
142− ContextPaths []string `json:"context_paths,omitempty" jsonschema:"description=Paths to files containing context information for the AI,example=.cursorrules,example=CRUSH.md"`
143− TUI *TUIOptions `json:"tui,omitempty" jsonschema:"description=Terminal user interface options"`
144− Debug bool `json:"debug,omitempty" jsonschema:"description=Enable debug logging,default=false"`
145− DebugLSP bool `json:"debug_lsp,omitempty" jsonschema:"description=Enable debug logging for LSP servers,default=false"`
146− DisableAutoSummarize bool `json:"disable_auto_summarize,omitempty" jsonschema:"description=Disable automatic conversation summarization,default=false"`
147− DataDirectory string `json:"data_directory,omitempty" jsonschema:"description=Directory for storing application data (relative to working directory),default=.crush,example=.crush"` // Relative to the cwd
148− DisabledTools []string `json:"disabled_tools" jsonschema:"description=Tools to disable"`
149− DisableProviderAutoUpdate bool `json:"disable_provider_auto_update,omitempty" jsonschema:"description=Disable providers auto-update,default=false"`
147+ ContextPaths []string `json:"context_paths,omitempty" jsonschema:"description=Paths to files containing context information for the AI,example=.cursorrules,example=CRUSH.md"`
148+ TUI *TUIOptions `json:"tui,omitempty" jsonschema:"description=Terminal user interface options"`
149+ Debug bool `json:"debug,omitempty" jsonschema:"description=Enable debug logging,default=false"`
150+ DebugLSP bool `json:"debug_lsp,omitempty" jsonschema:"description=Enable debug logging for LSP servers,default=false"`
151+ DisableAutoSummarize bool `json:"disable_auto_summarize,omitempty" jsonschema:"description=Disable automatic conversation summarization,default=false"`
152+ DataDirectory string `json:"data_directory,omitempty" jsonschema:"description=Directory for storing application data (relative to working directory),default=.crush,example=.crush"` // Relative to the cwd
153+ DisabledTools []string `json:"disabled_tools" jsonschema:"description=Tools to disable"`
154+ DisableProviderAutoUpdate bool `json:"disable_provider_auto_update,omitempty" jsonschema:"description=Disable providers auto-update,default=false"`
155+ Attribution *Attribution `json:"attribution,omitempty" jsonschema:"description=Attribution settings for generated content"`
150156}
151157
152158type MCPs map[string]MCPConfig
internal/llm/agent/agent.go+1−1
@@ -184,7 +184,7 @@ func NewAgent(
184184
185185 cwd := cfg.WorkingDir()
186186 allTools := []tools.BaseTool{
187− tools.NewBashTool(permissions, cwd),
187+ tools.NewBashTool(permissions, cwd, cfg.Options.Attribution),
188188 tools.NewDownloadTool(permissions, cwd),
189189 tools.NewEditTool(lspClients, permissions, history, cwd),
190190 tools.NewMultiEditTool(lspClients, permissions, history, cwd),
internal/llm/tools/bash.go+55−17
@@ -7,6 +7,7 @@ import (
77 "strings"
88 "time"
99
10+ "github.com/charmbracelet/crush/internal/config"
1011 "github.com/charmbracelet/crush/internal/permission"
1112 "github.com/charmbracelet/crush/internal/shell"
1213)
@@ -30,6 +31,7 @@ type BashResponseMetadata struct {
3031type bashTool struct {
3132 permissions permission.Service
3233 workingDir string
34+ attribution *config.Attribution
3335}
3436
3537const (
@@ -114,8 +116,53 @@ var bannedCommands = []string{
114116 "ufw",
115117}
116118
117−func bashDescription() string {
119+func (b *bashTool) bashDescription() string {
118120 bannedCommandsStr := strings.Join(bannedCommands, ", ")
121+
122+ // Build attribution text based on settings
123+ var attributionStep, attributionExample, prAttribution string
124+
125+ // Default to true if attribution is nil (backward compatibility)
126+ generatedWith := b.attribution == nil || b.attribution.GeneratedWith
127+ coAuthoredBy := b.attribution == nil || b.attribution.CoAuthoredBy
128+
129+ // Build PR attribution
130+ if generatedWith {
131+ prAttribution = "💘 Generated with Crush"
132+ }
133+
134+ if generatedWith || coAuthoredBy {
135+ attributionParts := []string{}
136+ if generatedWith {
137+ attributionParts = append(attributionParts, "💘 Generated with Crush")
138+ }
139+ if coAuthoredBy {
140+ attributionParts = append(attributionParts, "Co-Authored-By: Crush <crush@charm.land>")
141+ }
142+
143+ if len(attributionParts) > 0 {
144+ attributionStep = fmt.Sprintf("4. Create the commit with a message ending with:\n%s", strings.Join(attributionParts, "\n"))
145+
146+ attributionText := strings.Join(attributionParts, "\n ")
147+ attributionExample = fmt.Sprintf(`<example>
148+git commit -m "$(cat <<'EOF'
149+ Commit message here.
150+
151+ %s
152+ EOF
153+)"</example>`, attributionText)
154+ }
155+ }
156+
157+ if attributionStep == "" {
158+ attributionStep = "4. Create the commit with your commit message."
159+ attributionExample = `<example>
160+git commit -m "$(cat <<'EOF'
161+ Commit message here.
162+ EOF
163+)"</example>`
164+ }
165+
119166 return fmt.Sprintf(`Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
120167
121168CROSS-PLATFORM SHELL SUPPORT:
@@ -190,20 +237,10 @@ When the user asks you to create a new git commit, follow these steps carefully:
190237- Review the draft message to ensure it accurately reflects the changes and their purpose
191238</commit_analysis>
192239
193−4. Create the commit with a message ending with:
194−💘 Generated with Crush
195−Co-Authored-By: Crush <crush@charm.land>
240+%s
196241
197242- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:
198−<example>
199−git commit -m "$(cat <<'EOF'
200− Commit message here.
201−
202− 💘 Generated with Crush
203− Co-Authored-By: 💘 Crush <crush@charm.land>
204− EOF
205− )"
206−</example>
243+%s
207244
2082455. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them.
209246
@@ -262,14 +299,14 @@ gh pr create --title "the pr title" --body "$(cat <<'EOF'
262299## Test plan
263300[Checklist of TODOs for testing the pull request...]
264301
265−💘 Generated with Crush
302+%s
266303EOF
267304)"
268305</example>
269306
270307Important:
271308- Return an empty response - the user will see the gh output directly
272−- Never update git config`, bannedCommandsStr, MaxOutputLength)
309+- Never update git config`, bannedCommandsStr, MaxOutputLength, attributionStep, attributionExample, prAttribution)
273310}
274311
275312func blockFuncs() []shell.BlockFunc {
@@ -304,7 +341,7 @@ func blockFuncs() []shell.BlockFunc {
304341 }
305342}
306343
307−func NewBashTool(permission permission.Service, workingDir string) BaseTool {
344+func NewBashTool(permission permission.Service, workingDir string, attribution *config.Attribution) BaseTool {
308345 // Set up command blocking on the persistent shell
309346 persistentShell := shell.GetPersistentShell(workingDir)
310347 persistentShell.SetBlockFuncs(blockFuncs())
@@ -312,6 +349,7 @@ func NewBashTool(permission permission.Service, workingDir string) BaseTool {
312349 return &bashTool{
313350 permissions: permission,
314351 workingDir: workingDir,
352+ attribution: attribution,
315353 }
316354}
317355
@@ -322,7 +360,7 @@ func (b *bashTool) Name() string {
322360func (b *bashTool) Info() ToolInfo {
323361 return ToolInfo{
324362 Name: BashToolName,
325− Description: bashDescription(),
363+ Description: b.bashDescription(),
326364 Parameters: map[string]any{
327365 "command": map[string]any{
328366 "type": "string",
schema.json+20
@@ -3,6 +3,22 @@
33 "$id": "https://github.com/charmbracelet/crush/internal/config/config",
44 "$ref": "#/$defs/Config",
55 "$defs": {
6+ "Attribution": {
7+ "properties": {
8+ "co_authored_by": {
9+ "type": "boolean",
10+ "description": "Add Co-Authored-By trailer to commit messages",
11+ "default": true
12+ },
13+ "generated_with": {
14+ "type": "boolean",
15+ "description": "Add Generated with Crush line to commit messages and issues and PRs",
16+ "default": true
17+ }
18+ },
19+ "additionalProperties": false,
20+ "type": "object"
21+ },
622 "Config": {
723 "properties": {
824 "$schema": {
@@ -300,6 +316,10 @@
300316 "type": "boolean",
301317 "description": "Disable providers auto-update",
302318 "default": false
319+ },
320+ "attribution": {
321+ "$ref": "#/$defs/Attribution",
322+ "description": "Attribution settings for generated content"
303323 }
304324 },
305325 "additionalProperties": false,