Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 77 additions & 27 deletions crates/goose-mcp/src/developer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()]);

let mut global_hints_contents = Vec::with_capacity(hints_filenames.len());
let mut local_hints_contents = Vec::with_capacity(hints_filenames.len());

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())
});

if let Some(parent) = global_hints_path.parent() {
let _ = std::fs::create_dir_all(parent);
}

// Create the directory if it doesn't exist
let _ = std::fs::create_dir_all(global_hints_path.parent().unwrap());
if global_hints_path.is_file() {
if let Ok(content) = std::fs::read_to_string(&global_hints_path) {
global_hints_contents.push(content);
}
}

// Check for local hints in current directory
let local_hints_path = cwd.join(".goosehints");
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 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 !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"));
}

// 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);
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
Expand Down Expand Up @@ -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)]
Expand Down
4 changes: 2 additions & 2 deletions documentation/docs/guides/tips.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -45,4 +45,4 @@ You can turn a successful session into a reusable "[recipe](/docs/guides/recipes
You don’t 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.
4 changes: 4 additions & 0 deletions documentation/docs/guides/using-goosehints.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down
Loading