Merge pull request #2833 from divakantpandey/fix/2671-agent-config-symlinks

fix(cli): recognize symlinked agent config directories during detection (#2671)
This commit is contained in:
Martin Vogel authored and GitHub committed 2026-10-31 22:07:33 +01:00
commit af4121616e
2 files changed
+167 -1

No files matched your search

+20 -1
View File
@@ -2720,7 +2720,26 @@ int cbm_remove_zed_mcp_owned(const char *binary_path, const char *config_path) {
static bool dir_exists(const char *path) {
cbm_path_info_t info = {0};
return cbm_path_info_utf8(path, &info) == CBM_PATH_INFO_OK && info.is_directory;
if (cbm_path_info_utf8(path, &info) != CBM_PATH_INFO_OK) {
return false;
}
if (info.is_directory) {
return true;
}
#ifndef _WIN32
if (info.is_symlink) {
uid_t self = geteuid();
struct stat link_state;
if (lstat(path, &link_state) != 0 || !S_ISLNK(link_state.st_mode) ||
link_state.st_uid != self) {
return false;
}
struct stat target_state;
return stat(path, &target_state) == 0 && S_ISDIR(target_state.st_mode) &&
target_state.st_uid == self;
}
#endif
return false;
}
/* Resolve the Claude Code config dir.
+147
View File
@@ -6180,6 +6180,149 @@ TEST(cli_zed_mcp_jsonc_comments) {
PASS();
}
#ifndef _WIN32
TEST(cli_install_agent_config_symlinks_issue2671) {
/* Refs #2671: dotfiles checkout pattern where agent config directory is a
* user-owned symlink into dotfiles under the user's home directory. */
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm-symlink-test-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
FAIL("cbm_mkdtemp failed");
char dotfiles_zed[512];
snprintf(dotfiles_zed, sizeof(dotfiles_zed), "%s/dotfiles/zed", tmpdir);
test_mkdirp(dotfiles_zed);
char dotfiles_settings[512];
snprintf(dotfiles_settings, sizeof(dotfiles_settings), "%s/settings.json", dotfiles_zed);
write_test_file(dotfiles_settings, "{\"theme\": \"Ayu Dark\"}\n");
#ifdef __APPLE__
char parent_dir[512];
snprintf(parent_dir, sizeof(parent_dir), "%s/Library/Application Support", tmpdir);
test_mkdirp(parent_dir);
char zed_link[512];
snprintf(zed_link, sizeof(zed_link), "%s/Zed", parent_dir);
#else
char parent_dir[512];
snprintf(parent_dir, sizeof(parent_dir), "%s/.config", tmpdir);
test_mkdirp(parent_dir);
char zed_link[512];
snprintf(zed_link, sizeof(zed_link), "%s/zed", parent_dir);
#endif
char *saved_xdg = save_test_env("XDG_CONFIG_HOME");
char xdg_dir[640];
snprintf(xdg_dir, sizeof(xdg_dir), "%s/.config", tmpdir);
cbm_setenv("XDG_CONFIG_HOME", xdg_dir, 1); /* Linux resolver prefers XDG */
/* Dangling symlink and symlink to a regular file must remain undetected. */
char dangling_target[512];
snprintf(dangling_target, sizeof(dangling_target), "%s/dotfiles/missing-zed", tmpdir);
ASSERT_EQ(symlink(dangling_target, zed_link), 0);
bool dangling_detected = cbm_detect_agents(tmpdir).zed;
ASSERT_EQ(cbm_unlink(zed_link), 0);
ASSERT_EQ(symlink(dotfiles_settings, zed_link), 0);
bool file_link_detected = cbm_detect_agents(tmpdir).zed;
ASSERT_EQ(cbm_unlink(zed_link), 0);
/* Symlink resolving to a directory not owned by the invoking user must be refused. */
char foreign_dir[512];
const char *unowned_target = "/";
if (geteuid() == 0) {
snprintf(foreign_dir, sizeof(foreign_dir), "%s/dotfiles/foreign-zed", tmpdir);
test_mkdirp(foreign_dir);
ASSERT_EQ(chown(foreign_dir, 65534, 65534), 0);
unowned_target = foreign_dir;
}
ASSERT_EQ(symlink(unowned_target, zed_link), 0);
bool unowned_target_detected = cbm_detect_agents(tmpdir).zed;
ASSERT_EQ(cbm_unlink(zed_link), 0);
ASSERT_EQ(symlink(dotfiles_zed, zed_link), 0);
cbm_cli_set_client_selection_for_testing("zed");
int rc = cbm_install_agent_configs(tmpdir, "/usr/local/bin/codebase-memory-mcp", false, false);
cbm_cli_set_client_selection_for_testing(NULL);
restore_test_env("XDG_CONFIG_HOME", saved_xdg);
char *after = read_test_file_alloc(dotfiles_settings);
bool has_cbm = after && strstr(after, "codebase-memory-mcp") != NULL;
bool has_original = after && strstr(after, "Ayu Dark") != NULL;
free(after);
struct stat st;
ASSERT_EQ(lstat(zed_link, &st), 0);
bool still_symlink = S_ISLNK(st.st_mode);
test_rmdir_r(tmpdir);
ASSERT(!dangling_detected);
ASSERT(!file_link_detected);
ASSERT(!unowned_target_detected);
ASSERT_EQ(rc, 0);
ASSERT(has_cbm);
ASSERT(has_original);
ASSERT(still_symlink);
PASS();
}
TEST(cli_install_agent_config_file_symlink_issue2671) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm-file-symlink-test-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
FAIL("cbm_mkdtemp failed");
char dotfiles_zed[512];
snprintf(dotfiles_zed, sizeof(dotfiles_zed), "%s/dotfiles/zed", tmpdir);
test_mkdirp(dotfiles_zed);
char dotfiles_settings[512];
snprintf(dotfiles_settings, sizeof(dotfiles_settings), "%s/settings.json", dotfiles_zed);
write_test_file(dotfiles_settings, "{\"theme\": \"Ayu Dark\"}\n");
#ifdef __APPLE__
char zed_dir[512];
snprintf(zed_dir, sizeof(zed_dir), "%s/Library/Application Support/Zed", tmpdir);
test_mkdirp(zed_dir);
char zed_settings_link[512];
snprintf(zed_settings_link, sizeof(zed_settings_link), "%s/settings.json", zed_dir);
ASSERT_EQ(symlink("../../../dotfiles/zed/settings.json", zed_settings_link), 0);
#else
char zed_dir[512];
snprintf(zed_dir, sizeof(zed_dir), "%s/.config/zed", tmpdir);
test_mkdirp(zed_dir);
char zed_settings_link[512];
snprintf(zed_settings_link, sizeof(zed_settings_link), "%s/settings.json", zed_dir);
ASSERT_EQ(symlink("../../dotfiles/zed/settings.json", zed_settings_link), 0);
#endif
char *saved_xdg = save_test_env("XDG_CONFIG_HOME");
char xdg_dir[640];
snprintf(xdg_dir, sizeof(xdg_dir), "%s/.config", tmpdir);
cbm_setenv("XDG_CONFIG_HOME", xdg_dir, 1); /* Linux resolver prefers XDG */
cbm_cli_set_client_selection_for_testing("zed");
int rc = cbm_install_agent_configs(tmpdir, "/usr/local/bin/codebase-memory-mcp", false, false);
cbm_cli_set_client_selection_for_testing(NULL);
restore_test_env("XDG_CONFIG_HOME", saved_xdg);
char *after = read_test_file_alloc(dotfiles_settings);
bool has_cbm = after && strstr(after, "codebase-memory-mcp") != NULL;
bool has_original = after && strstr(after, "Ayu Dark") != NULL;
free(after);
struct stat st;
ASSERT_EQ(lstat(zed_settings_link, &st), 0);
bool still_symlink = S_ISLNK(st.st_mode);
test_rmdir_r(tmpdir);
ASSERT_EQ(rc, 0);
ASSERT(has_cbm);
ASSERT(has_original);
ASSERT(still_symlink);
PASS();
}
#endif
/* ═══════════════════════════════════════════════════════════════════
* PATH management tests (port of TestCLI_InstallPATHAppend)
* ═══════════════════════════════════════════════════════════════════ */
@@ -19250,6 +19393,10 @@ SUITE(cli) {
RUN_TEST(cli_zed_mcp_preserves_settings);
RUN_TEST(cli_zed_mcp_uninstall);
RUN_TEST(cli_zed_mcp_jsonc_comments);
#ifndef _WIN32
RUN_TEST(cli_install_agent_config_symlinks_issue2671);
RUN_TEST(cli_install_agent_config_file_symlink_issue2671);
#endif
/* PATH management (3 tests) */
RUN_TEST(cli_ensure_path_append);