﻿---
title: Use Symlinks to Keep LLM Configuration Files in Sync
date: 2026-03-15
tags:
  - Agent
  - OS
excerpt: "Coding agents use different project memory files like AGENTS.md, CLAUDE.md, and GEMINI.md. Symlinks let them share one source of truth."
updated: 2026-06-26 22:47:17
lang: en
i18n:
  cn: /use_symlink
  translation: 2
---

<script type="module" src="/js/components/tab.js"></script>

## Problems

The current **Coding Agent** ecosystem is in a lively, fast-moving stage. Different vendors and tools make different design and implementation choices, and no single standard has settled yet.

This is not unusual. It is even reasonable. The XDG Base Directory Specification[^1] says software configuration files should live under `$HOME/.config`, but not everyone follows it either (Claude Code, for example).

Michael Livshits[^2] summarized this in [Every CLI coding agent, compared](https://michaellivs.com/blog/cli-coding-agents-compared/). Even if we only look at Project Memory, there are already many names: `CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `.clinerules`, and so on.

| Agent        | MCP | Sandbox             | Sub-agents | Headless | Plan Mode | Project Memory      |
| ------------ | --- | ------------------- | ---------- | -------- | --------- | ------------------- |
| OpenCode     | Yes | Docker              | Yes        | Yes      | Yes       | AGENTS.md           |
| Claude Code  | Yes | Seatbelt/Bubblewrap | Yes        | Yes      | Yes       | CLAUDE.md           |
| Codex CLI    | Yes | Seatbelt/Landlock   | Yes        | Yes      | Yes       | AGENTS.md           |
| Gemini CLI   | Yes | Seatbelt/Docker     | Yes        | Yes      | Yes       | GEMINI.md           |
| Qwen Code    | Yes | Docker/Seatbelt     | Yes        | Yes      | Yes       | QWEN.md             |
| Aider        | No  | None                | No         | Yes      | No        | None                |
| Goose        | Yes | Docker (MCP)        | Yes        | Yes      | Yes       | .goosehints         |
| OpenHands    | Yes | Docker              | Yes        | Yes      | Yes       | None                |
| Continue CLI | Yes | None                | Yes        | Yes      | No        | .continue/rules     |
| Cline CLI    | Yes | Checkpoints         | Yes        | Yes      | Yes       | .clinerules         |
| Warp         | Yes | None                | No         | Yes      | Yes       | WARP.md (reads all) |

**Note**: OpenCode is mostly compatible with the Claude Code convention. As for Claude Code supporting `AGENTS.md`, who knows when that will happen: [Feature Request: Support AGENTS.md. · Issue #6235 · anthropics/claude-code](https://github.com/anthropics/claude-code/issues/6235)

## Solution

If a team uses multiple agents at the same time, manually maintaining several copies of `AGENTS.md/SKILL.md/...` is awkward. The fix is simple: create a **soft link** (also called a symbolic link or symlink) for the main configuration file.

<x-tabs>

<x-tab title="macOS/Linux" active>

```sh
# Use AGENTS.md as the primary file and create symlinks for other agents
ln -s ./AGENTS.md ./CLAUDE.md
ln -s ./AGENTS.md ./GEMINI.md
```

</x-tab>

<x-tab title="Windows">

```sh
# Note that mklink uses a different argument order from ln -s
# Ref: https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/mklink
mklink CLAUDE.md AGENTS.md
```

I have not opened my Windows machine in a long time...

</x-tab>

</x-tabs>

After that, whether you edit `AGENTS.md` or `CLAUDE.md`, you are editing the same file. The maintenance cost disappears.

## How It Works

A soft link is essentially a special file. If you inspect the files with eza, `CLAUDE.md` has file type `l` (symlink), while `AGENTS.md` has file type `.`.

```sh
$ eza --long AGENTS.md CLAUDE.md
.rw-r--r--@ 3.5k gjx 14 Mar 17:02 AGENTS.md
lrwxr-xr-x@    - gjx 15 Mar 18:33 CLAUDE.md -> ./AGENTS.md
```

When an application calls `{c} open("CLAUDE.md", O_RDONLY)`, the kernel detects during path resolution that `CLAUDE.md` is a symbolic link. It reads the target (`./AGENTS.md`) and keeps resolving the path until it reaches the final non-symlink inode. Then it creates a kernel-level open file object for that inode and returns a file descriptor. After that, `{c} read(fd, buffer, size)` reads directly from the target file. This is transparent to the application above it.

The path resolution process can be roughly illustrated by this LLM-generated Python code. Sigh, my coding ability and my ability to read non-Python code have both degraded.

<x-tabs>

<x-tab title="Code" active>

```py vided_real_path.py
import os

MAX_SYMLINK_DEPTH = 40

def real_path(path):
    path = os.path.abspath(path)
    depth = 0
    while True:
        if depth > MAX_SYMLINK_DEPTH:
            raise RuntimeError("symlink loop or too many levels")
        if not os.path.islink(path):
            return path
        target = os.readlink(path)
        if os.path.isabs(target):
            path = os.path.abspath(target)
        else:
            parent = os.path.dirname(path)
            path = os.path.normpath(os.path.join(parent, target))
        depth += 1

# Example usage
if __name__ == "__main__":
    print(real_path("CLAUDE.md"))
```

</x-tab>

<x-tab title="Result">

```sh
 $ python ./symlink_parser.py
/Users/gjx/Library/CloudStorage/OneDrive-Personal/Documents/posts/AGENTS.md
```

</x-tab>

</x-tabs>

## Git Compatibility

Symlinks can be committed to Git. For example, a repository may contain a link like this:

```sh
.agents/skills -> ../.claude/skills
```

After commit, Git records the relative path `../.claude/skills`. When someone else clones the repository on macOS/Linux, as long as `.claude/skills` is also inside the same repository, the symlink can still resolve to the right directory after checkout. The key is that the target path should be relative and should point inside the repository. Do not use an absolute path like `/Users/xxx/.../.claude/skills`, and do not point to something outside the repository. Otherwise, it can easily become a broken link on another machine.

The real caveat is Windows, or any Git environment where symlink checkout is disabled. In that case, Git may not create a real symlink. It may check it out as a plain text file whose content is `../.claude/skills`.

## Other Uses for Symlinks

Symlinks are everywhere in development toolchains:

- **pnpm**: uses symlinks to implement a content-addressable store. The same package only needs to live once on disk, and projects share it through links.
- **bun**: uses a similar symlink strategy to reduce the disk cost of `node_modules`. See my [[bun_link|post on bun link]], where I use `{sh} bun link` to simplify the workflow for developing a Library/Blog theme.
- **Homebrew**: symlinks programs installed under `Cellar/` into `/opt/homebrew/bin/`, which makes switching software versions convenient.

The earlier XDG Base Directory problem can also be handled with symlinks. You can collect configuration files scattered across `~/.config/`, `~/`, and other locations into one Git repository and manage them together. If you are interested, see my [[dotbot|post on dotbot]].

[^1]: Various specifications specify files and file formats. This specification defines where these files should be looked for by defining one or more base directories relative to which files should be located. [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir/latest/)
[^2]: Michael Livshits is a software engineer specializing in LLMs and AI systems. Previously Staff Engineer in ML/AI at Intuit. Founded a few startups before and after.

    He writes about LLMs, engineering, and the things he learns along the way at this blog.
