My AI Agent Knows What Project I'm Working On Before I Tell It

· 9 min read
My AI Agent Knows What Project I'm Working On Before I Tell It
Photo by Vishnu Mohanan on Unsplash

I kept repeating myself. Every time I opened Claude Code in my Obsidian vault, I'd type "let's work on the newsletter" and then spend the first few minutes pointing Claude at the right context files. Here's the publishing context. Here's the last session file. Here's the Signals doc. Every single time.

The vault already had all this context organized. 33 _context.md files tracking project state. 23 _last-session.md files with handoff notes. Signals documents tracking patterns over time. The information existed. Claude just didn't know to read it.

On the OpenClaw side (my Discord-based AI agent), this was already solved. Each Discord channel maps to a project. The agent knows which context to load based on which channel you're talking in. But Claude Code sessions are stateless. Every conversation starts fresh.

So I built a hook that fixes that.


The Problem with Instructions

The obvious first attempt: put "read the context files at session start" in CLAUDE.md. I tried that. It's guidance, not automation. Claude sees the instruction, and sometimes follows it. Sometimes doesn't. As conversations get long and context compresses, the instruction gets buried.

The second attempt: a hook that dumps all context files into every session. I have 23 session files. Most would be noise for any given conversation. "What's a good pasta recipe" doesn't need the DevX Team's last session loaded.

The real problem is intent detection. A hook fires before Claude processes your message. It can't understand what you're about to work on. It has to guess, and guessing wrong is worse than loading nothing.


Keyword Mapping

The solution is embarrassingly simple: a JSON file that maps keywords to projects.

{
  "id": "newsletter",
  "name": "Newsletter (Dev Notes)",
  "keywords": ["newsletter", "dev notes", "buttondown", "weekly newsletter"],
  "patterns": ["(write|draft|send).*?(newsletter|dev notes)"],
  "context": [
    "Publishing/_context.md",
    "Publishing/Newsletters/_context.md",
    "Publishing/Newsletters/_last-session.md"
  ],
  "suggestSkills": ["newsletter-writer", "writing-voice"],
  "priority": 2
}

When I say "let's draft tomorrow's newsletter," the hook matches "newsletter," loads the three context files, and suggests two relevant skills. No AI inference. No guessing. String matching.

The full config has 30 mappings covering every project area in the vault: Rula work projects, Modo consulting clients, publishing channels, personal projects, finances, fitness, even Cub Scouts. Each mapping defines which context files to load, which Signals doc to include, and which skills are relevant.


The Priority System

Keywords overlap. "Content" could mean the Content project or the content calendar skill. "Rula" could mean the top-level employer area or a specific sub-project like DevX.

The fix: priority levels. Higher numbers are more specific.

Priority Scope Example
0 Top-level area "all work", "personal overview"
1 Company/area "rula", "publishing"
2 Specific project "devx", "newsletter", "plex"
3 Sub-project "project1", "project2"

When "devx" matches at priority 2 and "rula" matches at priority 1, only DevX loads. The most specific match wins. When two things match at the same priority (like "content calendar" hitting both Content and Newsletter), both load. That's usually what you want.


The Hook

The hook is a bash script wired to UserPromptSubmit in Claude Code's settings. It runs every time I send a message:

  1. Reads my prompt from stdin (JSON with user_prompt field)
  2. Lowercases it and checks every keyword in the config
  3. Falls back to regex patterns if no keyword matched
  4. Filters by priority, keeping only the most specific matches
  5. Outputs the file paths Claude should read

The output looks like this:

=== PROJECT CONTEXT DETECTED ===
Matched: Content / Social, Newsletter (Dev Notes)

READ these files before responding (use the Read tool):
  - Publishing/_context.md
  - Publishing/Content/_context.md
  - Publishing/Content/_last-session.md
  - Publishing/Newsletters/_context.md
  - Publishing/Newsletters/_last-session.md
  - Publishing/Stats/Signals.md (Signals)

Relevant skills for this area:
  - /content-calendar
  - /social-coach
  - /writing-voice
  - /newsletter-writer

After completing work, suggest /handoff to save session state.
=== END PROJECT CONTEXT ===

Claude sees this injected into the conversation and reads the files before responding. No match means no output. "What's the weather" produces nothing. Zero overhead on conversations that don't need project context.

The hook also writes the matched project name to a temp file. More on that in a second.


Statusline: See It at a Glance

Claude Code has a configurable statusline at the bottom of the terminal. Mine already shows the repo name, git branch, context window usage, session cost, and lines changed. I added one more piece: the active project.

The context-loader hook writes the matched project name to .claude/hooks/.current-project. The statusline script reads that file and displays it right after the repo name:

Gray Matter ⟨Newsletter⟩ git:(main) | ctx: 12% | $0.42 | +35/-8 [Opus 4.6]

No match, no file, nothing shown. When the hook fires and matches "newsletter," the statusline updates. When I switch to a general conversation, it stays on the last project (which is usually what I want, since I'm likely still in that context).

The /ctx and /handoff skills also update this file, so the statusline stays in sync regardless of how the project was set. /handoff --clear removes the file entirely, resetting the statusline for when you're done with a project and switching direction.

It's a small thing, but glancing at the statusline and seeing ⟨DevX Team⟩ confirms the hook did its job without scrolling back to check the output.


Loading Children

Some conversations span a whole area. "Let's review all publishing stuff" shouldn't load just the top-level Publishing context. It should load every sub-project underneath.

The loadChildren flag handles this. When a parent mapping has loadChildren: true, the hook runs find on the base directory and loads every _context.md and _last-session.md it finds. Publishing has six sub-projects (Blog, Content, Newsletters, Podcast, Stats, plus the parent). One keyword loads all of them.


The Write Side: /handoff

Loading context is the read side. The write side is a /handoff skill that saves session state before I close a conversation.

When I run /handoff, Claude writes a structured _last-session.md with four sections: what was done, what needs review, deferred items, and next actions. Items from the previous session that weren't addressed get carried forward automatically. The project stays active in the statusline so the next message still has context.

/handoff --clear does the same save but removes the active project from the statusline. That's for when I'm done with a project and want to switch direction mid-session. The session state is saved, the statusline resets, and the next /ctx or keyword match picks up a new project.

The file is written for a fresh agent. No "as we discussed" or references to the conversation. Just facts, file paths, and concrete next steps. Because the next agent to read it might be Claude Code on my laptop, or OpenClaw on my phone through Discord. Either way, it needs to stand on its own.


Manual Override: /ctx

The hook handles 90% of cases. For the rest, there's /ctx.

/ctx                    → list all available projects
/ctx devx               → load DevX Team context
/ctx publishing         → load all Publishing context with children

This covers three scenarios: the hook matched the wrong project, I want to switch projects mid-conversation, or I'm starting a conversation that doesn't have obvious keywords ("let's pick up where we left off" doesn't match anything useful).


Cross-System Continuity

The key design choice: the _last-session.md files are the shared state between systems. They live in the Obsidian vault. Obsidian Sync keeps them current across devices. Both Claude Code and OpenClaw read and write the same files.

The hook itself only runs locally. It's wired in .claude/settings.local.json, which is gitignored. OpenClaw never sees it because OpenClaw doesn't need it. Discord channels already provide the project routing that the hook provides for Claude Code.

The session files are the contract between systems. The routing mechanism is system-specific.


What I'd Change

The keyword list needs real-world tuning. I seeded it with obvious terms from each project's context file, but I won't know the natural language I actually use until I've run a few weeks of sessions. The first round of edits will probably happen within days.

Regex patterns are powerful but fragile. (write|draft|send).*?(newsletter|dev notes) catches "draft the newsletter" but not "I need to finish that dev notes thing." I'm keeping patterns minimal and leaning on keywords. Patterns are the fallback, not the primary matching strategy.

No learning loop yet. The system doesn't track which keywords actually fired vs. which projects I ended up working on. A log file that captures "hook matched X, user actually worked on Y" would make tuning much faster. That's probably the next thing to build.


The Stack

For anyone building something similar:

  • Claude Code hooks (UserPromptSubmit) for automatic context injection
  • A JSON config mapping keywords to file paths (no code changes needed to add projects)
  • Claude Code skills for /ctx (manual load) and /handoff (session save)
  • Claude Code statusline to show the active project at a glance
  • .claude/settings.local.json to keep the hook local-only
  • Obsidian Sync to share session files across devices and systems

The hook script is about 130 lines of bash. The JSON config is where all the project knowledge lives. Adding a new project means adding a JSON object with keywords and file paths. No code changes.


How It Fits Together

This is the missing piece between sessions. The vault already had project context. OpenClaw already had cross-session continuity through Discord channels. Claude Code was the gap: powerful for deep work, but amnesiac between conversations.

Now the flow works end-to-end. Start a Claude Code session, mention the project, context loads automatically. Do the work. Run /handoff. Close the session. Open Discord on my phone, pick up in the same project channel, and OpenClaw reads the same _last-session.md that Claude Code just wrote. Switch back to Claude Code tomorrow, and the hook loads the same file again.

The context follows the work, not the tool.


If this sounds interesting to you, I'd love to chat with you about it. Find me on Bluesky or X.

Liked this? Get Dev Notes.

A weekly newsletter for developers. AI tools, Laravel, dev workflows, and things I find interesting. Every Friday at 8:45 AM Eastern.