Learn Claude Code: Custom Skills Dynamic Context Injection

Dynamic Context Injection

Intermediate 🕐 13 min Lesson 3 of 11
What you'll learn
  • Use the shell injection syntax to embed live command output into a skill before Claude reads it
  • Choose between inline and fenced block injection forms based on command complexity
  • Apply built-in substitution variables to make skills portable across sessions and install locations

The stale context problem

A skill that tells Claude "summarize the current git diff" without actually providing the diff is asking Claude to guess. Claude can describe what a diff might look like, or ask you to paste one, but it cannot read your working tree on its own. The result is either a generic response or an interruption.

Dynamic context injection solves this. Before Claude reads the skill content, Claude Code runs shell commands and replaces their placeholders with the actual output. By the time Claude sees anything, the live data is already there.

The inline injection syntax

Wrap any shell command in backticks and prefix it with ! at the start of a line:

!`git diff HEAD`

When the skill runs, Claude Code executes git diff HEAD and replaces the entire line with its output. Claude receives the rendered content — the actual diff — not the command itself. This is preprocessing, not something Claude executes. Claude only sees the result.

The rule about line position matters: the ! must appear at the start of a line or after whitespace. If it follows another character, as in KEY=!`cmd`, the placeholder is left as literal text and the command does not run.

Multi-line commands with fenced blocks

For commands that span multiple lines, or when you want to group several commands together, use a fenced block opened with ```!:

## Environment
```!
node --version
npm --version
git status --short
git log --oneline -5
```

Claude Code runs all commands in the block, concatenates their output, and replaces the entire fence with the result. Use this when you need a snapshot of several system states at once.

Practical injection patterns

Shell injection is most useful when the relevant data changes between sessions or per-project:

Git state: Pull the diff before a commit review, the recent log before writing a changelog, branch status before a deploy.

!`git diff HEAD`
!`git log --oneline -10`
!`git status --short`

Project config: Read package.json, pyproject.toml, or Cargo.toml to give Claude the correct package names and versions without hardcoding them into the skill.

!`cat package.json`

GitHub CLI: Pull live PR data so Claude can review actual changes rather than hypothetical ones.

!`gh pr diff`
!`gh pr view --comments`
!`gh pr diff --name-only`

Environment state: Check what is running, what environment variables are set, or what the current user is — useful for skills that behave differently in dev vs CI.

```!
echo "User: $USER"
echo "Branch: $(git branch --show-current)"
echo "Node: $(node --version 2>/dev/null || echo not installed)"
```

Built-in substitution variables

Claude Code provides several variables that expand inside skill content. These are not shell commands — they expand directly without the ! prefix:

Variable
Expands to
${CLAUDE_SESSION_ID}
Current session ID — useful for per-session log files
${CLAUDE_EFFORT}
Current effort level (low/medium/high/xhigh/max) — lets skills adapt instructions
${CLAUDE_SKILL_DIR}
Directory containing this SKILL.md — makes script paths portable
${CLAUDE_PROJECT_DIR}
Project root — reference project-local scripts from any skill location

The ${CLAUDE_SKILL_DIR} variable is especially important for skills that bundle scripts. A skill installed at ~/.claude/skills/visualize/ and later moved to a project at .claude/skills/visualize/ will still reference its bundled Python script correctly if the path uses ${CLAUDE_SKILL_DIR}/scripts/run.py instead of a hardcoded absolute path.

What injection cannot do

Injection runs once, before Claude reads anything. Command output is inserted as plain text and is not rescanned for further ! placeholders. A command cannot emit a placeholder that triggers a second round of injection.

The ! must appear at the start of a line or after whitespace. It does not work mid-line or inside quoted strings. For skills that require complex data transformation, run a script instead of chaining shell substitutions: !`python3 ${CLAUDE_SKILL_DIR}/scripts/prepare-context.py`.

Organizations can disable shell injection entirely via the disableSkillShellExecution managed setting. If you are building skills for a team, verify that this policy is not active before relying on injection for critical data — the placeholder is replaced with a policy message rather than command output, and the failure is silent.