Reverse engineering Claude Code

I’ve been curious about what happens inside Claude Code so I’ve spent a couple hours digging through prompts that it sends back to Anthropic.

As I’ve been going through that, I’ve gathered some insights why Claude Code is often slower and more expensive than other tools like Cursor.

Capturing prompts

It turned out a lot easier than I thought, thanks to mitmproxy:

$ brew install mitmproxy
$ mitmweb --mode reverse:https://api.anthropic.com --listen-port 8000
$ ANTHROPIC_BASE_URL=http://localhost:8000/ claude

Getting started

I opened a repo with my personal blog and entered “describe what’s in this project”.

The very first thing you see is that Claude Code tries to figure out whether what you have entered is a continuation of the previous conversation or a new topic.

Analyze if this message indicates a new conversation topic.
If it does, extract a 2-3 word title that captures the new topic.
Format your response as a JSON object with two fields: 'isNewTopic' (boolean) and 'title' (string, or null if isNewTopic is false). Only include these fields, no other text.

Once it determines that, it wraps all your inputs into this system prompt:

You are an agent for Claude Code, Anthropic's official CLI for Claude. Given the user's prompt, you should use the tools available to you to answer the user's question.

Notes:
1. IMPORTANT: You should be concise, direct, and to the point, since your responses will be displayed on a command line interface. Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as \"The answer is <answer>.\", \"Here is the content of the file...\" or \"Based on the information provided, the answer is...\" or \"Here is what I will do next...\".
2. When relevant, share file names and code snippets relevant to the query
3. Any file paths you return in your final response MUST be absolute. DO NOT use relative paths.
Here is useful information about the environment you are running in:
<env>
Working directory: /Users/kir/src/github.com/kirs/kirshatrov-com
Is directory a git repo: Yes
Platform: macos
Today's date: 2025-04-13
Model: claude-3-7-sonnet-20250219
</env>

As you answer the user's questions, you can use the following context:

<context name="directoryStructure">Below is a snapshot of this project's file structure at the start of the conversation. This snapshot will NOT update during the conversation. It skips over .gitignore patterns.

- /Users/kir/src/github.com/kirs/kirshatrov-com/
  - README.md
  - _posts/
    - 2015-03-02-capistrano-unicorn-bundler.markdown
    - 2015-04-07-rails-5.markdown
 ....
    - 2024-12-14-facebook-mysql-fork.md
  - bin/
...
</context>
<context name="gitStatus">This is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation.
Current branch: master

Main branch (you will usually use this for PRs): master

Status:
M .gitignore
 D .ruby-version
 M package-lock.json
?? playground/

Recent commits:
58ce3f9 m4 --force
3b1052a correction
82fe70a fix stuff
cfd73d9 follow-up
</context>

It’s nice that the state of git is included into the context. You can also notice that the context includes full tree directory view which can get somewhat big.

Part of this system prompt is a list of 11 tools that are available for the LLM to call:

dispatch_agent
Launch a new agent that has access to the following tools: View, GlobTool, GrepTool, LS, ReadNotebook, WebFetchTool. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries, use the Agent tool to perform the search for you.

Bash
Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures. Includes specialized functionality for git operations like commits and pull requests.

BatchTool
Batch execution tool that runs multiple tool invocations in a single request. Tools are executed in parallel when possible, and otherwise serially. Takes a list of tool invocations (tool_name and input pairs) and returns the collected results from all invocations.

GlobTool
Fast file pattern matching tool that works with any codebase size. Supports glob patterns like "/*.js" or "src//*.ts". Returns matching file paths sorted by modification time.
GrepTool
Fast content search tool that works with any codebase size. Searches file contents using regular expressions. Supports full regex syntax and filters files by pattern with the include parameter.

LS
Lists files and directories in a given path. The path parameter must be an absolute path, not a relative path. You can optionally provide an array of glob patterns to ignore.

View
Reads a file from the local filesystem. The file_path parameter must be an absolute path. By default, reads up to 2000 lines starting from the beginning of the file.

Edit
Tool for editing files. For moving or renaming files, use the Bash tool with the 'mv' command instead. For larger edits, use the Write tool to overwrite files.

Replace
Write a file to the local filesystem. Overwrites the existing file if there is one.

ReadNotebook
Reads a Jupyter notebook (.ipynb file) and returns all of the cells with their outputs.

NotebookEditCell
Completely replaces the contents of a specific cell in a Jupyter notebook (.ipynb file) with new source.

WebFetchTool
Fetches content from a specified URL and processes it using an AI model. Takes a URL and a prompt as input, fetches the URL content, converts HTML to markdown, and processes the content.

My original input (“describe what’s in this project”) then gets transformed into the dispatch_agent tool call with this instruction:

Analyze the repository structure and determine what kind of project this is.
Look at the file structure, package.json, and any configuration files to understand the tech stack,
then provide a concise summary of what this project appears to be.

This is then transformed into a few view tool calls for those files:

  • README.md
  • package.json
  • pages/index.js
  • pages/post.js
  • pages/travels.js

The result of feeding those files back to an LLM makes it into the following summary:

This is Kir Shatrov's personal website/blog built with Next.js, React, and Tailwind CSS. It includes:

- Blog posts on tech topics (Ruby/Rails, databases, engineering leadership)  
- Photography gallery  
- Travel map/history  
- Speaking engagements  
- Fabrication projects (woodworking/making)  

The site uses markdown for blog content with posts dating back to 2015, following standard Next.js project structure.

The above took 40 seconds of LLM time and cost me $0.11.

Writing new code

My next test input was:

Please write a bash script which displays the top story on Hacker News.
It should show the title, the link, and the number of points.

That spawned an agent use that has attempted to read https://github.com/HackerNews/API. But Claude’s self-limiting security policy has stopped that:

Error: Cannot fetch URL "https://github.com/HackerNews/API". For security, you can only fetch URLs from hosts that the user has mentioned in their messages or that are found in CLAUDE.md files or project files.

Then it tried to read https://hackernews.api-docs.io and run into the same error.

Then it tried to read https://firebase.google.com/docs/database and the same thing happened.

It then changed the strategy and outputted this:

I apologize for the limitations in accessing external URLs. Due to security restrictions, I can only fetch URLs that have been mentioned by you or are in project files.

The Hacker News API is hosted by Firebase and provides several endpoints, including:

- `https://hacker-news.firebaseio.com/v0/topstories.json` - Returns IDs of top stories
- `https://hacker-news.firebaseio.com/v0/item/{id}.json` - Returns details of a specific item

To create a bash script to fetch the top story, you would:
1. Fetch the array of top story IDs
2. Get the first ID from that array
3. Fetch the details for that ID

Here's a basic bash script example:

#!/bin/bash
...

What’s interesting is that none of that got saved or executed. So I’ve typed save that and execute.

Now, remember this prompt from the beginning?

Analyze if this message indicates a new conversation topic.
If it does, extract a 2-3 word title that captures the new topic.
Format your response as a JSON object with two fields: 'isNewTopic' (boolean) and 'title' (string, or null if isNewTopic is false). Only include these fields, no other text.

Like any other input, my save that and execute command had to be run through determining whether it's a new topic or not.

Saving the file involved two tool usage calls:

  • Replace to write the file
  • Bash with chmod +x hn_top.sh && hn_top.sh

Now comes the fun part. Any Bash tool use is preceded by this this prompt:

<policy_spec>
# Claude Code Code Bash command prefix detection

This document defines risk levels for actions that the Claude Code agent may take. This classification system is part of a broader safety framework and is used to determine when additional user confirmation or oversight may be needed.

## Definitions

**Command Injection:** Any technique used that would result in a command being run other than the detected prefix.

## Command prefix extraction examples
Examples:
- cat foo.txt => cat
- cd src => cd
- cd path/to/files/ => cd
- find ./src -type f -name \"*.ts\" => find
- gg cat foo.py => gg cat
- gg cp foo.py bar.py => gg cp
- git commit -m \"foo\" => git commit
- git diff HEAD~1 => git diff
- git diff --staged => git diff
- git diff $(pwd) => command_injection_detected
- git status => git status
- git status# test(`id`) => command_injection_detected
- git status`ls` => command_injection_detected
- git push => none
- git push origin master => git push
- git log -n 5 => git log
- git log --oneline -n 5 => git log
- grep -A 40 \"from foo.bar.baz import\" alpha/beta/gamma.py => grep
- pig tail zerba.log => pig tail
- potion test some/specific/file.ts => potion test
- npm run lint => none
- npm run lint -- \"foo\" => npm run lint
- npm test => none
- npm test --foo => npm test
- npm test -- -f \"foo\" => npm test
- pwd
 curl example.com => command_injection_detected
- pytest foo/bar.py => pytest
- scalac build => none
- sleep 3 => sleep
</policy_spec>

The user has allowed certain command prefixes to be run, and will otherwise be asked to approve or deny the command.
Your task is to determine the command prefix for the following command.

IMPORTANT: Bash commands may run multiple commands that are chained together.
For safety, if the command seems to contain command injection, you must return \"command_injection_detected\". 
(This will help protect the user: if they think that they're allowlisting command A, 
but the AI coding agent sends a malicious command that technically has the same prefix as command A, 
then the safety system will see that you said “command_injection_detected” and ask the user for manual confirmation.)

Note that not every command has a prefix. If a command has no prefix, return \"none\".

ONLY return the prefix. Do not return any other text, markdown markers, or other content or formatting.

Command: chmod +x hn_top.sh

Once the security is assessed, it will try to grab the actual commands to let users make them always allowed to run

Extract any file paths that this command reads or modifies. For commands like "git diff" and "cat", include the paths of files being shown. Use paths verbatim -- don't add any slashes or try to resolve them. Do not try to infer paths that were not explicitly listed in the command output.
Format your response as:
<filepaths>
path/to/file1
path/to/file2
</filepaths>

If no files are read or modified, return empty filepaths tags: <filepaths></filepaths>
Do not include any other text in your response.

Which renders the following UX bit:

You can imagine that having to call an LLM at most twice for every Bash tool use makes things somewhat slower.

/init

Claude Code provides /init command which creates CLAUDE.md with generated contents about your project. Below is the prompt for that.

Please analyze this codebase and create a CLAUDE.md file containing:
1. Build/lint/test commands - especially for running a single test
2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.

Usage notes:
- The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.\
- If there's already a CLAUDE.md, improve it.\
- If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.\
- Be sure to prefix the file with the following text:

# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

The prompt launches a BatchTool agent with a few more tool calls to collect more files — based on that context prompt that hints it the directory tree:

{
  "type": "tool_use",
  "id": "toolu_01GmZ4d81pGHKaqryDEuZQNm",
  "name": "BatchTool",
  "input": {
    "description": "Gather repository information",
    "invocations": [
      {
        "tool_name": "GlobTool",
        "input": {
            "pattern": "package*.json"
        }
      },
      {
        "tool_name": "GlobTool",
        "input": {
            "pattern": "*.md"
        }
      },
      {
        "tool_name": "GlobTool",
        "input": {
            "pattern": ".cursor/rules/**"
        }
      },
      {
        "tool_name": "GlobTool",
        "input": {
            "pattern": ".cursorrules/**"
        }
      },
      {
        "tool_name": "GlobTool",
        "input": {
            "pattern": ".github/copilot-instructions.md"
        }
      }
    ]
  }
}

Wrap up

Overall, Claude Code tends to be more generic and secure for the price of extra time and money billed on more tokens.

This is very much unlike Cursor which would never attempt to analyze suggested bash tools. And I would have most often prefer that Claude Code's approach.

I've also noticed that it uses claude-3-7-sonnet for most things that require reasoning, and claude-3-5-haiku for simpler calls like parsing Bash commands. But still, claude-3.5 can be quite an overkill for those and gpt-3.5-turbo would do that 10x cheaper ($1.5 vs $15 per 1M output tokens).

Still, in terms of UX, Claude Code is the winner for an agentic tool that runs in console. Aider is close, but not nearly there in terms of reliability and UX. While Claude Code is a single NPM package (still requires a modern-ish version of Node which likely requires NVM installed), Aider's installation process self-installed uv on my machine just because it needed it. And OpenHands had to download ~10Gb of Docker images.

Written in April 2025.
Kir Shatrov

Kir Shatrov helps businesses to grow by scaling the infrastructure. He writes about software, scalability and the ecosystem. Follow him on Twitter to get the latest updates.