feat: Allow configuring hints filename(s) (#3269)

Signed-off-by: Rami Chowdhury <rami.chowdhury@gmail.com>
This commit is contained in:
Rami Chowdhury
2025-07-31 13:06:08 -04:00
committed by GitHub
parent 71c8d9e7aa
commit d9313aca57
3 changed files with 83 additions and 29 deletions
+77 -27
View File
@@ -461,40 +461,57 @@ impl DeveloperRouter {
},
};
// choose_app_strategy().config_dir()
// - macOS/Linux: ~/.config/goose/
// - Windows: ~\AppData\Roaming\Block\goose\config\
// keep previous behavior of expanding ~/.config in case this fails
let global_hints_path = choose_app_strategy(crate::APP_STRATEGY.clone())
.map(|strategy| strategy.in_config_dir(".goosehints"))
.unwrap_or_else(|_| {
PathBuf::from(shellexpand::tilde("~/.config/goose/.goosehints").to_string())
});
let hints_filenames: Vec<String> = std::env::var("CONTEXT_FILE_NAMES")
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_else(|| vec![".goosehints".to_string()]);
// Create the directory if it doesn't exist
let _ = std::fs::create_dir_all(global_hints_path.parent().unwrap());
let mut global_hints_contents = Vec::with_capacity(hints_filenames.len());
let mut local_hints_contents = Vec::with_capacity(hints_filenames.len());
// Check for local hints in current directory
let local_hints_path = cwd.join(".goosehints");
for hints_filename in &hints_filenames {
// Global hints
// choose_app_strategy().config_dir()
// - macOS/Linux: ~/.config/goose/
// - Windows: ~\AppData\Roaming\Block\goose\config\
// keep previous behavior of expanding ~/.config in case this fails
let global_hints_path = choose_app_strategy(crate::APP_STRATEGY.clone())
.map(|strategy| strategy.in_config_dir(hints_filename))
.unwrap_or_else(|_| {
let path_str = format!("~/.config/goose/{}", hints_filename);
PathBuf::from(shellexpand::tilde(&path_str).to_string())
});
// Read global hints if they exist
let mut hints = String::new();
if global_hints_path.is_file() {
if let Ok(global_hints) = std::fs::read_to_string(&global_hints_path) {
hints.push_str("\n### Global Hints\nThe developer extension includes some global hints that apply to all projects & directories.\n");
hints.push_str(&global_hints);
if let Some(parent) = global_hints_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if global_hints_path.is_file() {
if let Ok(content) = std::fs::read_to_string(&global_hints_path) {
global_hints_contents.push(content);
}
}
let local_hints_path = cwd.join(hints_filename);
if local_hints_path.is_file() {
if let Ok(content) = std::fs::read_to_string(&local_hints_path) {
local_hints_contents.push(content);
}
}
}
// Read local hints if they exist
if local_hints_path.is_file() {
if let Ok(local_hints) = std::fs::read_to_string(&local_hints_path) {
if !hints.is_empty() {
hints.push_str("\n\n");
}
hints.push_str("### Project Hints\nThe developer extension includes some hints for working on the project in this directory.\n");
hints.push_str(&local_hints);
let mut hints = String::new();
if !global_hints_contents.is_empty() {
hints.push_str("\n### Global Hints\nThe developer extension includes some global hints that apply to all projects & directories.\n");
hints.push_str(&global_hints_contents.join("\n"));
}
if !local_hints_contents.is_empty() {
if !hints.is_empty() {
hints.push_str("\n\n");
}
hints.push_str("### Project Hints\nThe developer extension includes some hints for working on the project in this directory.\n");
hints.push_str(&local_hints_contents.join("\n"));
}
// Return base instructions directly when no hints are found
@@ -1749,6 +1766,39 @@ mod tests {
temp_dir.close().unwrap();
}
#[test]
#[serial]
fn test_goosehints_multiple_filenames() {
let dir = TempDir::new().unwrap();
std::env::set_current_dir(dir.path()).unwrap();
std::env::set_var("CONTEXT_FILE_NAMES", r#"["CLAUDE.md", ".goosehints"]"#);
fs::write("CLAUDE.md", "Custom hints file content from CLAUDE.md").unwrap();
fs::write(".goosehints", "Custom hints file content from .goosehints").unwrap();
let router = DeveloperRouter::new();
let instructions = router.instructions();
assert!(instructions.contains("Custom hints file content from CLAUDE.md"));
assert!(instructions.contains("Custom hints file content from .goosehints"));
std::env::remove_var("CONTEXT_FILE_NAMES");
}
#[test]
#[serial]
fn test_goosehints_configurable_filename() {
let dir = TempDir::new().unwrap();
std::env::set_current_dir(dir.path()).unwrap();
std::env::set_var("CONTEXT_FILE_NAMES", r#"["CLAUDE.md"]"#);
fs::write("CLAUDE.md", "Custom hints file content").unwrap();
let router = DeveloperRouter::new();
let instructions = router.instructions();
assert!(instructions.contains("Custom hints file content"));
assert!(!instructions.contains(".goosehints")); // Make sure it's not loading the default
std::env::remove_var("CONTEXT_FILE_NAMES");
}
#[tokio::test]
#[serial]
#[cfg(windows)]
+2 -2
View File
@@ -27,7 +27,7 @@ LLMs have context windows, which are limits on how much conversation history the
Turning on too many extensions can degrade performance. Enable only essential [extensions and tools](/docs/guides/managing-tools/tool-permissions) to improve tool selection accuracy, save context window space, and stay within provider tool limits.
### Teach Goose your preferences
Help Goose remember how you like to work by using [`.goosehints`](/docs/guides/using-goosehints/) for permanent project preferences and the [Memory extension](/docs/mcp/memory-mcp) for things you want Goose to dynamically recall later. Both can help save valuable context window space while keeping your preferences available.
Help Goose remember how you like to work by using [`.goosehints` or other context files](/docs/guides/using-goosehints/) for permanent project preferences and the [Memory extension](/docs/mcp/memory-mcp) for things you want Goose to dynamically recall later. Both can help save valuable context window space while keeping your preferences available.
### Protect sensitive files
Goose is often eager to make changes. You can stop it from changing specific files by creating a [.gooseignore](/docs/guides/using-gooseignore) file. In this file, you can list all the file paths you want it to avoid.
@@ -45,4 +45,4 @@ You can turn a successful session into a reusable "[recipe](/docs/guides/recipes
You dont need to get it right the first time. Iterating on prompts and tools is part of the workflow.
### Keep Goose updated
Regularly [update](/docs/guides/updating-goose) Goose to benefit from the latest features, bug fixes, and performance improvements.
Regularly [update](/docs/guides/updating-goose) Goose to benefit from the latest features, bug fixes, and performance improvements.
@@ -39,6 +39,10 @@ Goose supports two types of hint files:
You can use both global and local hints at the same time. When both exist, Goose will consider both your global preferences and project-specific requirements. If the instructions in your local hints file conflict with your global preferences, Goose will prioritize the local hints.
:::
:::tip
You can name your context files differently -- e.g. `AGENTS.md` -- and Goose can still pick them up. Configure the `CONTEXT_FILE_NAMES` setting!
:::
<Tabs groupId="interface">
<TabItem value="ui" label="Goose Desktop" default>