Detailed changes
@@ -173,6 +173,39 @@ $HOME/.local/share/crush/crush.json
%LOCALAPPDATA%\crush\crush.json
```
+### Attribution Settings
+
+By default, Crush adds attribution information to git commits and pull requests it creates. You can customize this behavior with the `attribution` option:
+
+```json
+{
+ "$schema": "https://charm.land/crush.json",
+ "options": {
+ "attribution": {
+ "co_authored_by": true,
+ "generated_with": true
+ }
+ }
+}
+```
+
+- `co_authored_by`: When true (default), adds `Co-Authored-By: Crush <crush@charm.land>` to commit messages
+- `generated_with`: When true (default), adds `💘 Generated with Crush` line to commit messages and PR descriptions
+
+To disable all attribution, set both options to false:
+
+```json
+{
+ "$schema": "https://charm.land/crush.json",
+ "options": {
+ "attribution": {
+ "co_authored_by": false,
+ "generated_with": false
+ }
+ }
+}
+```
+
### LSPs
Crush can use LSPs for additional context to help inform its decisions, just
@@ -138,15 +138,21 @@ type Permissions struct {
SkipRequests bool `json:"-"` // Automatically accept all permissions (YOLO mode)
}
+type Attribution struct {
+ CoAuthoredBy bool `json:"co_authored_by,omitempty" jsonschema:"description=Add Co-Authored-By trailer to commit messages,default=true"`
+ GeneratedWith bool `json:"generated_with,omitempty" jsonschema:"description=Add Generated with Crush line to commit messages and issues and PRs,default=true"`
+}
+
type Options struct {
- ContextPaths []string `json:"context_paths,omitempty" jsonschema:"description=Paths to files containing context information for the AI,example=.cursorrules,example=CRUSH.md"`
- TUI *TUIOptions `json:"tui,omitempty" jsonschema:"description=Terminal user interface options"`
- Debug bool `json:"debug,omitempty" jsonschema:"description=Enable debug logging,default=false"`
- DebugLSP bool `json:"debug_lsp,omitempty" jsonschema:"description=Enable debug logging for LSP servers,default=false"`
- DisableAutoSummarize bool `json:"disable_auto_summarize,omitempty" jsonschema:"description=Disable automatic conversation summarization,default=false"`
- 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
- DisabledTools []string `json:"disabled_tools" jsonschema:"description=Tools to disable"`
- DisableProviderAutoUpdate bool `json:"disable_provider_auto_update,omitempty" jsonschema:"description=Disable providers auto-update,default=false"`
+ ContextPaths []string `json:"context_paths,omitempty" jsonschema:"description=Paths to files containing context information for the AI,example=.cursorrules,example=CRUSH.md"`
+ TUI *TUIOptions `json:"tui,omitempty" jsonschema:"description=Terminal user interface options"`
+ Debug bool `json:"debug,omitempty" jsonschema:"description=Enable debug logging,default=false"`
+ DebugLSP bool `json:"debug_lsp,omitempty" jsonschema:"description=Enable debug logging for LSP servers,default=false"`
+ DisableAutoSummarize bool `json:"disable_auto_summarize,omitempty" jsonschema:"description=Disable automatic conversation summarization,default=false"`
+ 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
+ DisabledTools []string `json:"disabled_tools" jsonschema:"description=Tools to disable"`
+ DisableProviderAutoUpdate bool `json:"disable_provider_auto_update,omitempty" jsonschema:"description=Disable providers auto-update,default=false"`
+ Attribution *Attribution `json:"attribution,omitempty" jsonschema:"description=Attribution settings for generated content"`
}
type MCPs map[string]MCPConfig
@@ -184,7 +184,7 @@ func NewAgent(
cwd := cfg.WorkingDir()
allTools := []tools.BaseTool{
- tools.NewBashTool(permissions, cwd),
+ tools.NewBashTool(permissions, cwd, cfg.Options.Attribution),
tools.NewDownloadTool(permissions, cwd),
tools.NewEditTool(lspClients, permissions, history, cwd),
tools.NewMultiEditTool(lspClients, permissions, history, cwd),
@@ -7,6 +7,7 @@ import (
"strings"
"time"
+ "github.com/charmbracelet/crush/internal/config"
"github.com/charmbracelet/crush/internal/permission"
"github.com/charmbracelet/crush/internal/shell"
)
@@ -30,6 +31,7 @@ type BashResponseMetadata struct {
type bashTool struct {
permissions permission.Service
workingDir string
+ attribution *config.Attribution
}
const (
@@ -114,8 +116,53 @@ var bannedCommands = []string{
"ufw",
}
-func bashDescription() string {
+func (b *bashTool) bashDescription() string {
bannedCommandsStr := strings.Join(bannedCommands, ", ")
+
+ // Build attribution text based on settings
+ var attributionStep, attributionExample, prAttribution string
+
+ // Default to true if attribution is nil (backward compatibility)
+ generatedWith := b.attribution == nil || b.attribution.GeneratedWith
+ coAuthoredBy := b.attribution == nil || b.attribution.CoAuthoredBy
+
+ // Build PR attribution
+ if generatedWith {
+ prAttribution = "💘 Generated with Crush"
+ }
+
+ if generatedWith || coAuthoredBy {
+ attributionParts := []string{}
+ if generatedWith {
+ attributionParts = append(attributionParts, "💘 Generated with Crush")
+ }
+ if coAuthoredBy {
+ attributionParts = append(attributionParts, "Co-Authored-By: Crush <crush@charm.land>")
+ }
+
+ if len(attributionParts) > 0 {
+ attributionStep = fmt.Sprintf("4. Create the commit with a message ending with:\n%s", strings.Join(attributionParts, "\n"))
+
+ attributionText := strings.Join(attributionParts, "\n ")
+ attributionExample = fmt.Sprintf(`<example>
+git commit -m "$(cat <<'EOF'
+ Commit message here.
+
+ %s
+ EOF
+)"</example>`, attributionText)
+ }
+ }
+
+ if attributionStep == "" {
+ attributionStep = "4. Create the commit with your commit message."
+ attributionExample = `<example>
+git commit -m "$(cat <<'EOF'
+ Commit message here.
+ EOF
+)"</example>`
+ }
+
return fmt.Sprintf(`Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
CROSS-PLATFORM SHELL SUPPORT:
@@ -190,20 +237,10 @@ When the user asks you to create a new git commit, follow these steps carefully:
- Review the draft message to ensure it accurately reflects the changes and their purpose
</commit_analysis>
-4. Create the commit with a message ending with:
-💘 Generated with Crush
-Co-Authored-By: Crush <crush@charm.land>
+%s
- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:
-<example>
-git commit -m "$(cat <<'EOF'
- Commit message here.
-
- 💘 Generated with Crush
- Co-Authored-By: 💘 Crush <crush@charm.land>
- EOF
- )"
-</example>
+%s
5. 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.
@@ -262,14 +299,14 @@ gh pr create --title "the pr title" --body "$(cat <<'EOF'
## Test plan
[Checklist of TODOs for testing the pull request...]
-💘 Generated with Crush
+%s
EOF
)"
</example>
Important:
- Return an empty response - the user will see the gh output directly
-- Never update git config`, bannedCommandsStr, MaxOutputLength)
+- Never update git config`, bannedCommandsStr, MaxOutputLength, attributionStep, attributionExample, prAttribution)
}
func blockFuncs() []shell.BlockFunc {
@@ -304,7 +341,7 @@ func blockFuncs() []shell.BlockFunc {
}
}
-func NewBashTool(permission permission.Service, workingDir string) BaseTool {
+func NewBashTool(permission permission.Service, workingDir string, attribution *config.Attribution) BaseTool {
// Set up command blocking on the persistent shell
persistentShell := shell.GetPersistentShell(workingDir)
persistentShell.SetBlockFuncs(blockFuncs())
@@ -312,6 +349,7 @@ func NewBashTool(permission permission.Service, workingDir string) BaseTool {
return &bashTool{
permissions: permission,
workingDir: workingDir,
+ attribution: attribution,
}
}
@@ -322,7 +360,7 @@ func (b *bashTool) Name() string {
func (b *bashTool) Info() ToolInfo {
return ToolInfo{
Name: BashToolName,
- Description: bashDescription(),
+ Description: b.bashDescription(),
Parameters: map[string]any{
"command": map[string]any{
"type": "string",
@@ -3,6 +3,22 @@
"$id": "https://github.com/charmbracelet/crush/internal/config/config",
"$ref": "#/$defs/Config",
"$defs": {
+ "Attribution": {
+ "properties": {
+ "co_authored_by": {
+ "type": "boolean",
+ "description": "Add Co-Authored-By trailer to commit messages",
+ "default": true
+ },
+ "generated_with": {
+ "type": "boolean",
+ "description": "Add Generated with Crush line to commit messages and issues and PRs",
+ "default": true
+ }
+ },
+ "additionalProperties": false,
+ "type": "object"
+ },
"Config": {
"properties": {
"$schema": {
@@ -300,6 +316,10 @@
"type": "boolean",
"description": "Disable providers auto-update",
"default": false
+ },
+ "attribution": {
+ "$ref": "#/$defs/Attribution",
+ "description": "Attribution settings for generated content"
}
},
"additionalProperties": false,