From e24058b7a872054e3e76f9301d64eccccb75811d Mon Sep 17 00:00:00 2001 From: Gav Verma Date: Mon, 2 Feb 2026 16:49:23 -0800 Subject: [PATCH] feat: Read personal skills from .agents/skills (#10437) - Issue: https://github.com/agentskills/agentskills/issues/15 - Follow-up to https://github.com/openai/codex/pull/10317 (for team/repo skills) - This change now also loads personal/user skills from `$HOME/.agents/skills` (or `~/.agents/skills`) in addition to loading from `.agents/skills` inside of git repos. - The location of `.system` skills remains unchanged. - Keeping backwards compatibility with `~/.codex/skills` for now until we fully deprecate. With skills in both personal folders: image We load from both places: image --- codex-rs/Cargo.lock | 1 + codex-rs/core/Cargo.toml | 1 + codex-rs/core/src/skills/loader.rs | 92 +++++++++++++++++++++++++++--- 3 files changed, 86 insertions(+), 8 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 57186f82f..716bc4cd5 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1422,6 +1422,7 @@ dependencies = [ "core-foundation 0.9.4", "core_test_support", "ctor 0.6.3", + "dirs", "dunce", "encoding_rs", "env-flags", diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index a857da8b3..a95981cca 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -45,6 +45,7 @@ codex-utils-pty = { workspace = true } codex-utils-readiness = { workspace = true } codex-utils-string = { workspace = true } codex-windows-sandbox = { package = "codex-windows-sandbox", path = "../windows-sandbox-rs" } +dirs = { workspace = true } dunce = { workspace = true } encoding_rs = { workspace = true } env-flags = { workspace = true } diff --git a/codex-rs/core/src/skills/loader.rs b/codex-rs/core/src/skills/loader.rs index a3c7731a6..a04f3089b 100644 --- a/codex-rs/core/src/skills/loader.rs +++ b/codex-rs/core/src/skills/loader.rs @@ -13,6 +13,7 @@ use crate::skills::model::SkillToolDependency; use crate::skills::system::system_cache_root_dir; use codex_app_server_protocol::ConfigLayerSource; use codex_protocol::protocol::SkillScope; +use dirs::home_dir; use dunce::canonicalize as canonicalize_path; use serde::Deserialize; use std::collections::HashSet; @@ -164,7 +165,10 @@ where outcome } -fn skill_roots_from_layer_stack_inner(config_layer_stack: &ConfigLayerStack) -> Vec { +fn skill_roots_from_layer_stack_inner( + config_layer_stack: &ConfigLayerStack, + home_dir: Option<&Path>, +) -> Vec { let mut roots = Vec::new(); for layer in @@ -182,12 +186,21 @@ fn skill_roots_from_layer_stack_inner(config_layer_stack: &ConfigLayerStack) -> }); } ConfigLayerSource::User { .. } => { - // `$CODEX_HOME/skills` (user-installed skills). + // Deprecated user skills location (`$CODEX_HOME/skills`), kept for backward + // compatibility. roots.push(SkillRoot { path: config_folder.as_path().join(SKILLS_DIR_NAME), scope: SkillScope::User, }); + // `$HOME/.agents/skills` (user-installed skills). + if let Some(home_dir) = home_dir { + roots.push(SkillRoot { + path: home_dir.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME), + scope: SkillScope::User, + }); + } + // Embedded system skills are cached under `$CODEX_HOME/skills/.system` and are a // special case (not a config layer). roots.push(SkillRoot { @@ -220,15 +233,16 @@ fn skill_roots(config: &Config) -> Vec { #[cfg(test)] pub(crate) fn skill_roots_from_layer_stack( config_layer_stack: &ConfigLayerStack, + home_dir: Option<&Path>, ) -> Vec { - skill_roots_from_layer_stack_inner(config_layer_stack) + skill_roots_from_layer_stack_inner(config_layer_stack, home_dir) } pub(crate) fn skill_roots_from_layer_stack_with_agents( config_layer_stack: &ConfigLayerStack, cwd: &Path, ) -> Vec { - let mut roots = skill_roots_from_layer_stack_inner(config_layer_stack); + let mut roots = skill_roots_from_layer_stack_inner(config_layer_stack, home_dir().as_deref()); roots.extend(repo_agents_skill_roots(config_layer_stack, cwd)); dedupe_skill_roots_by_path(&mut roots); roots @@ -829,7 +843,8 @@ mod tests { let tmp = tempfile::tempdir()?; let system_folder = tmp.path().join("etc/codex"); - let user_folder = tmp.path().join("home/codex"); + let home_folder = tmp.path().join("home"); + let user_folder = home_folder.join("codex"); fs::create_dir_all(&system_folder)?; fs::create_dir_all(&user_folder)?; @@ -853,7 +868,7 @@ mod tests { ConfigRequirementsToml::default(), )?; - let got = skill_roots_from_layer_stack(&stack) + let got = skill_roots_from_layer_stack(&stack, Some(&home_folder)) .into_iter() .map(|root| (root.scope, root.path)) .collect::>(); @@ -862,6 +877,10 @@ mod tests { got, vec![ (SkillScope::User, user_folder.join("skills")), + ( + SkillScope::User, + home_folder.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME) + ), ( SkillScope::System, user_folder.join("skills").join(".system") @@ -877,7 +896,8 @@ mod tests { fn skill_roots_from_layer_stack_includes_disabled_project_layers() -> anyhow::Result<()> { let tmp = tempfile::tempdir()?; - let user_folder = tmp.path().join("home/codex"); + let home_folder = tmp.path().join("home"); + let user_folder = home_folder.join("codex"); fs::create_dir_all(&user_folder)?; let project_root = tmp.path().join("repo"); @@ -906,7 +926,7 @@ mod tests { ConfigRequirementsToml::default(), )?; - let got = skill_roots_from_layer_stack(&stack) + let got = skill_roots_from_layer_stack(&stack, Some(&home_folder)) .into_iter() .map(|root| (root.scope, root.path)) .collect::>(); @@ -916,6 +936,10 @@ mod tests { vec![ (SkillScope::Repo, dot_codex.join("skills")), (SkillScope::User, user_folder.join("skills")), + ( + SkillScope::User, + home_folder.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME) + ), ( SkillScope::System, user_folder.join("skills").join(".system") @@ -926,6 +950,55 @@ mod tests { Ok(()) } + #[test] + fn loads_skills_from_home_agents_dir_for_user_scope() -> anyhow::Result<()> { + let tmp = tempfile::tempdir()?; + + let home_folder = tmp.path().join("home"); + let user_folder = home_folder.join("codex"); + fs::create_dir_all(&user_folder)?; + + let user_file = AbsolutePathBuf::from_absolute_path(user_folder.join("config.toml"))?; + let layers = vec![ConfigLayerEntry::new( + ConfigLayerSource::User { file: user_file }, + TomlValue::Table(toml::map::Map::new()), + )]; + let stack = ConfigLayerStack::new( + layers, + ConfigRequirements::default(), + ConfigRequirementsToml::default(), + )?; + + let skill_path = write_skill_at( + &home_folder.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME), + "agents-home", + "agents-home-skill", + "from home agents", + ); + + let outcome = + load_skills_from_roots(skill_roots_from_layer_stack(&stack, Some(&home_folder))); + assert!( + outcome.errors.is_empty(), + "unexpected errors: {:?}", + outcome.errors + ); + assert_eq!( + outcome.skills, + vec![SkillMetadata { + name: "agents-home-skill".to_string(), + description: "from home agents".to_string(), + short_description: None, + interface: None, + dependencies: None, + path: normalized(&skill_path), + scope: SkillScope::User, + }] + ); + + Ok(()) + } + fn write_skill(codex_home: &TempDir, dir: &str, name: &str, description: &str) -> PathBuf { write_skill_at(&codex_home.path().join("skills"), dir, name, description) } @@ -2136,6 +2209,9 @@ interface: .map(|root| root.scope) .collect(); let mut expected = vec![SkillScope::User, SkillScope::System]; + if home_dir().is_some() { + expected.insert(1, SkillScope::User); + } if cfg!(unix) { expected.push(SkillScope::Admin); }