diff --git a/README.md b/README.md
index f3ddaf7afa23014f3b0bb75297e0cac3da81318b..2c2559282624fbc7732b3ca7687f624e434c0065 100644
--- a/README.md
+++ b/README.md
@@ -402,6 +402,33 @@ down. There is a short grace window right after `POST /v1/workspaces` so a
client that has created the workspace but not yet opened its event stream
does not get reaped before it can attach.
+### Global context files
+
+Crush automatically includes two files for cross-project instructions.
+
+- `~/.config/crush/CRUSH.md`: Crush-specific rules that would confuse other
+ agentic coding tools. If you only use Crush, this is the only one you need to
+ edit.
+- `~/.config/AGENTS.md`: generic instructions that other coding tools might
+ read. Avoid referring to Crush-specific features or workflows here. You
+ probably only care about this if you use multiple agentic coding tools and
+ want to share instructions between them.
+
+You can customize these paths using the `global_context_paths` option in your
+configuration:
+
+```jsonc
+{
+ "$schema": "https://charm.land/crush.json",
+ "options": {
+ "global_context_paths": [
+ "~/path/to/custom/context/file.md",
+ "/full/path/to/folder/of/files/" // recursively load all .md files in folder
+ ]
+ }
+}
+```
+
### Ignoring Files
Crush respects `.gitignore` files by default, but you can also create a
diff --git a/internal/agent/common_test.go b/internal/agent/common_test.go
index 8eb0f10c4ac35eee9b6dd76835d11d12cb233f74..bb1d4397827eff387f87b93c3e5d4b69a95c794e 100644
--- a/internal/agent/common_test.go
+++ b/internal/agent/common_test.go
@@ -152,6 +152,7 @@ func coderAgent(r *vcr.Recorder, env fakeEnv, large, small fantasy.LanguageModel
cfg.Config().Options.SkillsPaths = nil
cfg.Config().Options.DisabledSkills = []string{"crush-config"}
cfg.Config().Options.ContextPaths = nil
+ cfg.Config().Options.GlobalContextPaths = nil
cfg.Config().LSP = nil
systemPrompt, err := prompt.Build(context.TODO(), large.Provider(), large.Model(), cfg)
diff --git a/internal/agent/prompt/prompt.go b/internal/agent/prompt/prompt.go
index 7609661f31940ac9bccde40334034dc1e16191f1..4917b25b2280700d068eaa6f2f53d4507ecb90ff 100644
--- a/internal/agent/prompt/prompt.go
+++ b/internal/agent/prompt/prompt.go
@@ -29,16 +29,17 @@ type Prompt struct {
}
type PromptDat struct {
- Provider string
- Model string
- Config config.Config
- WorkingDir string
- IsGitRepo bool
- Platform string
- Date string
- GitStatus string
- ContextFiles []ContextFile
- AvailSkillXML string
+ Provider string
+ Model string
+ Config config.Config
+ WorkingDir string
+ IsGitRepo bool
+ Platform string
+ Date string
+ GitStatus string
+ ContextFiles []ContextFile
+ GlobalContextFiles []ContextFile
+ AvailSkillXML string
}
type ContextFile struct {
@@ -147,22 +148,27 @@ func expandPath(path string, store *config.ConfigStore) string {
return path
}
-func (p *Prompt) promptData(ctx context.Context, provider, model string, store *config.ConfigStore) (PromptDat, error) {
- workingDir := cmp.Or(p.workingDir, store.WorkingDir())
- platform := cmp.Or(p.platform, runtime.GOOS)
-
+// loadContextFiles loads and deduplicates context files from a list of paths.
+func loadContextFiles(paths []string, store *config.ConfigStore) map[string][]ContextFile {
files := map[string][]ContextFile{}
-
- cfg := store.Config()
- for _, pth := range cfg.Options.ContextPaths {
+ for _, pth := range paths {
expanded := expandPath(pth, store)
pathKey := strings.ToLower(expanded)
if _, ok := files[pathKey]; ok {
continue
}
- content := processContextPath(expanded, store)
- files[pathKey] = content
+ files[pathKey] = processContextPath(expanded, store)
}
+ return files
+}
+
+func (p *Prompt) promptData(ctx context.Context, provider, model string, store *config.ConfigStore) (PromptDat, error) {
+ workingDir := cmp.Or(p.workingDir, store.WorkingDir())
+ platform := cmp.Or(p.platform, runtime.GOOS)
+
+ cfg := store.Config()
+ contextFiles := loadContextFiles(cfg.Options.ContextPaths, store)
+ globalContextFiles := loadContextFiles(cfg.Options.GlobalContextPaths, store)
// Discover and load skills metadata.
var availSkillXML string
@@ -217,8 +223,11 @@ func (p *Prompt) promptData(ctx context.Context, provider, model string, store *
}
}
- for _, contextFiles := range files {
- data.ContextFiles = append(data.ContextFiles, contextFiles...)
+ for _, files := range contextFiles {
+ data.ContextFiles = append(data.ContextFiles, files...)
+ }
+ for _, files := range globalContextFiles {
+ data.GlobalContextFiles = append(data.GlobalContextFiles, files...)
}
return data, nil
}
diff --git a/internal/agent/templates/coder.md.tpl b/internal/agent/templates/coder.md.tpl
index 79b9e1af3ba56ef9be81352b9e04bb9374c69ee8..bfa06d6c23f019b492b2a3c86c4c4986c4ef99f3 100644
--- a/internal/agent/templates/coder.md.tpl
+++ b/internal/agent/templates/coder.md.tpl
@@ -396,11 +396,25 @@ If a skill mentions scripts, references, or assets, they live in the same folder
{{end}}
{{if .ContextFiles}}
-
+# Project-Specific Context
+Make sure to follow the instructions in the context below.
+
{{range .ContextFiles}}
{{.Content}}
{{end}}
-
+
+{{end}}
+{{if .GlobalContextFiles}}
+
+# User context
+The following is personal content added by the user that they'd like you to follow no matter what project you're working in.
+
+{{range .GlobalContextFiles}}
+
+{{.Content}}
+
+{{end}}
+
{{end}}
diff --git a/internal/config/config.go b/internal/config/config.go
index 38c8159109dbcaf945ceb3a00626fa1f0c34048a..abd36f3e71023393d6de7bfcb61f1d4d3c40bdc9 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -260,6 +260,7 @@ func (Attribution) JSONSchemaExtend(schema *jsonschema.Schema) {
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"`
+ GlobalContextPaths []string `json:"global_context_paths,omitempty" jsonschema:"description=Paths to files containing global context information for the AI,default=~/.config/crush/CRUSH.md,default=~/.config/AGENTS.md"`
SkillsPaths []string `json:"skills_paths,omitempty" jsonschema:"description=Paths to directories containing Agent Skills (folders with SKILL.md files),example=~/.config/crush/skills,example=./skills"`
TUI *TUIOptions `json:"tui,omitempty" jsonschema:"description=Terminal user interface options"`
Debug bool `json:"debug,omitempty" jsonschema:"description=Enable debug logging,default=false"`
diff --git a/internal/config/load.go b/internal/config/load.go
index 2f0946e7bc45ca2e3dcc17c79cba1dc30106d975..8551070e17cb38fea1f59058c3322a859a8df59e 100644
--- a/internal/config/load.go
+++ b/internal/config/load.go
@@ -417,6 +417,16 @@ func (c *Config) setDefaults(workingDir, dataDir string) {
if c.Options.TUI == nil {
c.Options.TUI = &TUIOptions{}
}
+ if len(c.Options.GlobalContextPaths) == 0 {
+ crushConfigDir := filepath.Dir(GlobalConfig())
+ c.Options.GlobalContextPaths = []string{
+ filepath.Join(crushConfigDir, "CRUSH.md"),
+ filepath.Join(filepath.Dir(crushConfigDir), "AGENTS.md"),
+ }
+ }
+ slices.Sort(c.Options.GlobalContextPaths)
+ c.Options.GlobalContextPaths = slices.Compact(c.Options.GlobalContextPaths)
+
if dataDir != "" {
c.Options.DataDirectory = dataDir
} else if c.Options.DataDirectory == "" {
@@ -448,6 +458,7 @@ func (c *Config) setDefaults(workingDir, dataDir string) {
// Add the default context paths if they are not already present
c.Options.ContextPaths = append(slices.Clone(defaultContextPaths), c.Options.ContextPaths...)
+
slices.Sort(c.Options.ContextPaths)
c.Options.ContextPaths = slices.Compact(c.Options.ContextPaths)
diff --git a/schema.json b/schema.json
index 9580a7be217151b8d6f2543071fb635f10763016..5fc0e0ac498bbf9a96ac287c1c75840f6d1886c6 100644
--- a/schema.json
+++ b/schema.json
@@ -405,6 +405,17 @@
"type": "array",
"description": "Paths to files containing context information for the AI"
},
+ "global_context_paths": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Paths to files containing global context information for the AI",
+ "default": [
+ "~/.config/crush/CRUSH.md",
+ "~/.config/AGENTS.md"
+ ]
+ },
"skills_paths": {
"items": {
"type": "string",