Auto-Inject Project Context into Claude Code Slash Commands

hero

Every time you crack open a new Claude Code session, do you find yourself typing the same boilerplate — "this project runs Node 20, port 3000, tests use Jest, the main branch is…"? That ritual burns real time. Across a team, it's death by a thousand context pastes. This tutorial shows you how to wire $ARGUMENTS and shell substitution into Claude Code's custom slash commands so your project metadata lands in the prompt automatically — no copy-paste, no re-explaining, no drift.

overall flow diagram

Section 1: The Problem — Repetitive Context Is Killing Your Flow

Here's the loop most developers are stuck in. You open a session, tell Claude what stack you're on, paste in a file list, name the branch, describe the test runner. Then tomorrow you do it again. And the next day.

The mechanical cost is real — I clocked it at roughly 90 seconds per session for a mid-size project. Multiply that by five sessions a day and you're at 7–8 minutes burned on pure logistics. But the deeper cost is the flow break: every time you re-explain context, you interrupt the work you were actually trying to do.

My first instinct was to keep a snippet file and paste from it. That broke immediately — the branch name went stale the moment I switched, and the file list was always one commit behind.

# What the "before" session looks like — manual, error-prone
# You type all of this by hand, every time:
Project: my-api-server
Node: 20.11.0
Branch: feat/auth-refactor
Changed files: src/auth/jwt.ts, src/middleware/guard.ts
Test runner: Jest
Port: 3000

Please review for security vulnerabilities.

manual context entry pain points

The stale-snippet problem is what pushed me toward shell substitution. If the shell evaluates $(git branch --show-current) at call time, it can never be stale.

Section 2: The Fix — Custom Commands with Shell Substitution

Claude Code reads markdown files from .claude/commands/ and exposes them as /project:<filename> slash commands. Drop a file called review.md in there and you get /project:review. That's the foundation.

The two ingredients that make it powerful are:

  1. $ARGUMENTS — a reserved variable that captures everything you type after the slash command name
  2. $(...) shell expressions — evaluated live when the command runs, so the output is always current

Here's a minimal but complete example:

# Step 1: create the directory
mkdir -p .claude/commands

# Step 2: write the command file
cat > .claude/commands/review.md << 'EOF'
---
description: Code review with auto-injected project context
allowed-tools: [Read, Bash, Grep]
---

Project: $PROJECT_NAME
Node version: $NODE_VERSION
Current branch: $(git branch --show-current)
Changed files: $(git diff --name-only HEAD~1)

Review the code above from a **$ARGUMENTS** perspective.
EOF

Now when you type /project:review security vulnerabilities inside a Claude Code session, the prompt Claude actually receives looks like this:

Project: my-api-server
Node version: v20.11.0
Current branch: feat/auth-refactor
Changed files: src/auth/jwt.ts src/middleware/guard.ts

Review the code above from a security vulnerabilities perspective.

The $ARGUMENTS value — "security vulnerabilities" — slots in exactly where you put the variable. The shell expressions pull live data from git. The environment variables ($PROJECT_NAME, $NODE_VERSION) come from a source file we'll set up next.

fixed prompt injection flow

Verification — after setting this up, run the command and check that the injected values are correct:

# Quick sanity check: print what the shell expressions resolve to
echo "Branch: $(git branch --show-current)"
echo "Changed: $(git diff --name-only HEAD~1)"
# Expected output (example):
# Branch: feat/auth-refactor
# Changed: src/auth/jwt.ts
#          src/middleware/guard.ts

If either returns empty, you're either not in a git repo or HEAD~1 doesn't exist yet (first commit edge case — swap to git diff --name-only HEAD instead).

Section 3: Scaling It — .env.claude for Team Standardization

Hard-coding $PROJECT_NAME into the command file defeats the point — you'd need to edit the file per project. The cleaner approach is a .env.claude file at the project root that acts as the single source of truth for Claude-facing metadata.

# .env.claude — project root, committed to the repo
PROJECT_NAME=my-api-server
NODE_VERSION=$(node -v)
API_BASE_URL=http://localhost:3000
TEST_RUNNER=jest
DB_ENGINE=postgres
# .claude/commands/debug.md — references the env file
---
description: Debug session with full project context
allowed-tools: [Read, Bash, Grep, Write]
---

$(source .env.claude && echo "Project: $PROJECT_NAME")
$(source .env.claude && echo "Node: $NODE_VERSION")
$(source .env.claude && echo "Test runner: $TEST_RUNNER")
$(source .env.claude && echo "API base: $API_BASE_URL")
Current branch: $(git branch --show-current)
Recent commits: $(git log --oneline -5)

Debug the following issue with full awareness of the stack above:
$ARGUMENTS

When a new engineer joins the team, they clone the repo, get .env.claude automatically (it's committed), and every slash command works identically on their machine. No onboarding doc needed for "which port does this run on."

team standardization structure

Section 4: Variations and Gotchas

Multiple command files for different roles. A review.md for PR reviews, a debug.md for runtime issues, a test.md that pipes in failing test output — each can have its own allowed-tools set and inject different slices of context. They all live in .claude/commands/ and don't interfere with each other.

.claude/commands/
├── review.md    → /project:review
├── debug.md     → /project:debug
├── test.md      → /project:test
└── deploy.md    → /project:deploy

Shell substitution timing. The $(...) expressions are evaluated when the command is invoked, not when the file is read. This is what you want — it means $(git diff --name-only HEAD~1) always reflects the current state of the repo, not the state from when you wrote the command.

The HEAD~1 edge case. If you're on the very first commit of a branch, HEAD~1 doesn't exist and git will return an error. Guard against it:

# Safer version that falls back to HEAD if HEAD~1 doesn't exist
Changed files: $(git diff --name-only HEAD~1 2>/dev/null || git diff --name-only HEAD)

Mac vs. Linux differences. On macOS, $(node -v) in .env.claude only evaluates if .env.claude is sourced in a bash/zsh context. If you're on a nix-based CI runner that uses sh, some bashisms won't work — keep the expressions POSIX-compatible or evaluate them eagerly in CI before invoking Claude Code.

Don't put secrets in .env.claude. It's committed to the repo. Keep it to non-sensitive metadata — project name, port numbers, tool choices. Real secrets stay in .env (which is gitignored) and should never flow into a Claude prompt anyway.

Variable type Where to put it Committed?
Project name, port, stack .env.claude ✅ Yes
API keys, DB passwords .env ❌ No
Branch / file context Shell $(...) in command file N/A (runtime)
User-provided focus area $ARGUMENTS N/A (runtime)

Timing benchmark. What I measured on an M4 Mac: command file load to completed prompt injection averaged 0.3 seconds. Session time dropped from ~90 seconds (manual context entry) to under 50 seconds. The 40-second saving per session sounds modest, but the bigger win is that the context is always accurate — no stale branch name, no missing files.

Closing

The real value isn't the 40 seconds you save per session. It's that you stop context-switching into "project explainer" mode every time you open Claude Code. The command handles the boilerplate; you handle the actual question.

Set up .claude/commands/ today, commit it alongside .env.claude, and every future session on this project — and every teammate's session — starts from a correct, complete context automatically.

Next logical step: wire allowed-tools tightly per command so a review.md can't accidentally trigger write operations, and a deploy.md can explicitly allow the Bash commands it needs.


🐦 Faster updates on X: @baegseungh7061
📚 More in this series: Code Practical
💌 Subscribe: Follow on X or grab the RSS

댓글