After posts on prompts for SRE/SysAdmin/DevOps, local and open source agents, and development with AI, and a long run of aggregators and self-hosted models, my daily setup settled on three tools:
- Claude Code for heavy coding work and as a terminal agent
- AbacusAI (ChatLLM, with RouteLLM picking the model behind the scenes)
- OpenCode as the open source CLI/TUI for everything else, with the OpenCode Go and Z.AI Coding Plan (GLM) subscriptions
I still test new tools. What I stopped doing is rebuilding my workflow for each one. The shared base is one instruction file, one skills folder, RTK, and hooks for the rules that can’t depend on the model.
Update (October 2026): Claude Code now reads
AGENTS.mdnatively, OpenCode v2 loads the global and projectAGENTS.mdtogether (the first version of this post said it didn’t, which was wrong), and~/.agents/skills/became the skills folder most tools read. The post below describes the current state, and the setup got simpler for it.
RTK: token savings
RTK (Rust Token Killer) is a CLI proxy that rewrites shell commands before execution to save tokens, condensing git/kubectl/terraform output before it reaches the context. It’s a single Rust binary with no dependencies.
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh
Alternatives: brew install rtk-ai/tap/rtk, or cargo install --git https://github.com/rtk-ai/rtk --branch master rtk (plain cargo install rtk installs an unrelated crate, “Rust Type Kit”).
rtk init -g # Claude Code: hook + ~/.claude/RTK.md + @RTK.md + settings.json
rtk init -g --opencode # OpenCode: installs only the plugin
rtk init --show # shows what is installed, per tool
rtk init -g registers a PreToolUse hook in Claude Code, writes ~/.claude/RTK.md for the agent to consult, and adds @RTK.md to ~/.claude/CLAUDE.md. That’s why my AGENTS.md ends with that import.
Don’t trust --show alone. After the move to OpenCode v2 it reported [ok] plugin installed while OpenCode refused to load the plugin, which was still written for the v1 plugin API. RTK 1.0.1 fixed it. To check, run a command through OpenCode and look for it as rtk ... in rtk gain --history; when the plugin fails, ~/.local/share/opencode/log/opencode.log has a failed to load plugin line.
One AGENTS.md for every tool
My global instruction file is in ai-md-stack. It covers writing style, commit conventions, and how the agent should approach a change. An excerpt:
# Global Rules
- Write codes and comments in English by default; use Brazilian Portuguese only when explicitly requested.
- Do not use the em dash (U+2014, `—`, `—`) as punctuation; use a comma, colon, or rewrite.
## Git Commits
- Commit messages MUST describe only the *what* and *why* of the change.
- Follow Conventional Commits v1.0.0: https://www.conventionalcommits.org/en/v1.0.0/
- Subject line: imperative mood, 50 chars max, no trailing period.
## Imports
@RTK.md
OpenCode and AbacusAI read AGENTS.md. Claude Code reads it too since v2.1.277, but only at project scope: there is no user-level equivalent, so ~/.claude/AGENTS.md is ignored unless ~/.claude/CLAUDE.md points to it. The canonical file lives in ~/.claude/ and the other tools link to it:
$EDITOR ~/.claude/AGENTS.md # canonical file
cd ~/.claude && ln -s AGENTS.md CLAUDE.md # relative symlink, same folder
ln -s ~/.claude/AGENTS.md ~/.config/opencode/AGENTS.md
OpenCode also falls back to ~/.claude/CLAUDE.md when it finds no global AGENTS.md (OPENCODE_DISABLE_CLAUDE_CODE=1 turns that off), but the symlink makes the source of truth explicit.
Project scope
Each tool’s /init (Claude Code, OpenCode, and AbacusAI since 2.10) writes a different file. On the same empty repository, Claude Code left placeholders, while OpenCode with Kimi K2 refused to guess the toolchain at all. So I let each /init run on its own, and write the project AGENTS.md by hand when I want the same text everywhere.
A project with only AGENTS.md now works in all three tools. The ln -s AGENTS.md CLAUDE.md I used to add per repository is no longer needed, but it’s harmless: Claude Code reads the content once. Two details:
- A
CLAUDE.local.mdcounts as aCLAUDE.mdand makes Claude Code stop readingAGENTS.md. To load both, set Project instructions toclaude-md-and-agents-mdin/config. - If you used a
SessionStarthook to printAGENTS.mdinto Claude Code, remove it, or the file gets loaded twice.
For Claude Code-specific rules on top of the shared file, a project CLAUDE.md can import it:
@AGENTS.md
## Claude Code
- Use plan mode for changes under src/billing/.
Cowork ignores the global file
In a Claude Cowork session on desktop, project files and project imports work normally. The user scope doesn’t: a ~/.claude/CLAUDE.md that is a symlink is skipped, and so is an @import in it that resolves outside the session’s working directory. My setup trips both. The only way to get global rules into Cowork is pasting the text literally into ~/.claude/CLAUDE.md, at the cost of keeping two copies in sync.
AbacusAI CLI only reads the project file
Abacus’s documentation says AGENTS.md works “at the user and project level”. To check, I put a behavior rule (“always end the response with tag X”) in ~/AGENTS.md and ran abacusai -p "What's 2+2?" in a folder with no project file. No tag. With the same rule in the project’s AGENTS.md, the tag appeared, and --no-agents-md removed it again. The CLI has no user-level AGENTS.md; the “user level” in the docs applies to skills. For AbacusAI, every project needs its own AGENTS.md.
Imports and precedence
@path imports are Claude Code syntax. OpenCode doesn’t expand them; its equivalent is the instructions field in opencode.json, which takes paths, globs, and URLs and combines them with the AGENTS.md files:
{
"instructions": ["docs/guidelines.md", ".cursor/rules/*.md"]
}
AbacusAI has neither: its AGENTS.md is one file, read whole.
How the global and project files combine:
- Claude Code concatenates managed policy,
~/.claude/CLAUDE.md, the project’sCLAUDE.md(or itsAGENTS.mdwhen there’s noCLAUDE.md), andCLAUDE.local.md. The project file comes last, but if it contradicts the global one, the model decides which rule wins. - OpenCode takes one file per category and combines them: the first local file walking up from the cwd (
AGENTS.mdbeforeCLAUDE.md) and the global~/.config/opencode/AGENTS.md. - AbacusAI only reads the project file.
I tested OpenCode v2.0.29 with one rule per file in an empty git repository:
# global (~/.config/opencode/AGENTS.md)
- Always end every response with the literal tag [GLOBAL-OK].
# project (./AGENTS.md)
- Always start every response with the literal tag [PROJECT-OK].
$ opencode run --standalone -m opencode-go/glm-5.3-flash "What is 2+2?"
[PROJECT-OK] 2 + 2 = **4** [GLOBAL-OK]
Both apply, so there’s no need for an instructions entry pointing back at the global file. The same project file under claude -p printed [PROJECT-OK].
When testing instruction changes in OpenCode v2, use --standalone or opencode service restart first: opencode run talks to a background service by default, and in my test it kept applying an AGENTS.md I had already deleted.
Hooks for rules that must hold
AGENTS.md is context. The model tries to follow it, and in a long session it may not. A hook is code the client runs before or after a tool call, and it can block or rewrite the call whatever the model decided. Anything that must always hold goes into a hook.
Hooks don’t port between tools. Claude Code uses PreToolUse/PostToolUse in settings.json. OpenCode uses JS/TS plugins in ~/.config/opencode/plugins/ (or .opencode/plugins/) with events like tool.execute.before, so porting a hook means rewriting it in TypeScript. AbacusAI documents nothing equivalent.
Mine are in ai-md-stack: settings.example.json, four hooks, and a shared library, all shellcheck-clean. _common.sh checks whether a command is actually invoked, so terraform plan triggers a guard and jq --arg c 'terraform plan' doesn’t:
is_command_invoked() {
local line="$1" name="$2"
line=$(printf '%s' "$line" | tr '\n' ';')
line=$(printf '%s' "$line" \
| sed -E "s/'[^']*'/''/g" \
| sed -E 's/"[^"]*"/""/g')
printf '%s' "$line" | grep -qE "(^|[;&|(]|&&|\|\|)[[:space:]]*(sudo[[:space:]]+)?${name}([[:space:]]|$)"
}
Without it, a guard fires whenever the command name appears as data, and a guard with false positives gets ignored.
mutation-guard.sh(PreToolUse): blocks write verbs onaws/gh/kubectl/terraform. With it on,aws:*andgh:*can be fully allowed insettings.json, since the hook does the blocking. Escape hatch per command:ALLOW_MUTATION=1 <command>.commit-guard.sh(PreToolUse): validatesgit commitagainst the commit rules inAGENTS.md: valid type, 50 characters, no trailing period, no tooling trailer.session-context.sh(SessionStart): injects the current branch (with a warning onmain/master), the dirty file count, and the last commit.redact-secrets.sh(PostToolUse): redacts leaked secrets (GitHub token, OpenAI or Anthropic key, AWS access key, PEM block), trims fixed noise, and truncates output above 30 KB, keeping the start and the end. It usesupdatedToolOutput, which replaces what the model sees.
The InstructionsLoaded event is useful for confirming an imported or symlinked file reached the session. It doesn’t fire for an AGENTS.md that Claude Code reads natively, only for one a CLAUDE.md imports or links to.
Skills: one folder, ~/.agents/skills
All three tools use the same skill format: a directory named after the skill with a SKILL.md that has name/description frontmatter. They differ in where they look:
- Claude Code: only
~/.claude/skills/ - OpenCode:
~/.config/opencode/skills/,~/.claude/skills/, and~/.agents/skills/ - AbacusAI:
~/.abacusai/skills/and~/.agents/skills/, plus auto-managed built-ins in~/.abacusai/builtin-skills/
~/.agents/skills/ is read by every tool except Claude Code, and it’s where the skills CLI (npx skills add <repo>) installs, recording each source in ~/skills-lock.json and creating the Claude Code symlink itself. That makes it the canonical folder. Skills I write live in their own repositories, with one link into ~/.agents/skills/ and a second one for Claude Code:
ln -s ~/GIT/humanize-br ~/.agents/skills/humanize-br
ln -s ../../.agents/skills/humanize-br ~/.claude/skills/humanize-br
$ ls -l ~/.agents/skills/ ~/.claude/skills/
/home/esli/.agents/skills/:
cf-cli/
humanize-br -> /home/esli/GIT/humanize-br
v-repo-audit/
...
/home/esli/.claude/skills/:
cf-cli -> ../../.agents/skills/cf-cli
humanize-br -> ../../.agents/skills/humanize-br
v-repo-audit -> ../../.agents/skills/v-repo-audit
...
Asking each tool to list its skills showed the same seven in all three. Don’t keep another copy in ~/.config/opencode/skills/: OpenCode reads all three folders, and with duplicate names only the first one found loads. The frontmatter name must match the directory name, and Claude Code ignores symlinked user files in some sessions (Cowork again), so invoke a skill to confirm it loaded.
Conclusion
The clients are converging, so every workaround needs re-testing after a major upgrade. In this update, two of mine had become unnecessary, one claim was wrong, and the RTK plugin for OpenCode had stopped loading without any visible error.
Full package: github.com/Esl1h/ai-md-stack.
