AI & ML

What Claude Code Actually Chooses: Research Reveals AI Tool Preferences

· 5 min read
SitePoint Premium
Stay Relevant and Grow Your Career in Tech
  • Premium Results
  • Publish articles on SitePoint
  • Daily curated jobs
  • Learning Paths
  • Discounts to dev tools
Start Free Trial

7 Day Free Trial. Cancel Anytime.

As AI coding agents take on more architectural responsibility, the tools they silently select shape entire projects. Informal testing conducted in June 2025 against Claude Code version 1.0.12 has begun quantifying what many developers suspected: these agents carry measurable biases.

Table of Contents

Why AI Tool Preferences Matter

As AI coding agents take on more architectural responsibility, the tools they silently select shape entire projects. Informal testing conducted in June 2025 against Claude Code version 1.0.12 has begun quantifying what many developers suspected: these agents carry measurable biases. The practice of "vibe coding," where developers describe what they want and let AI handle implementation details, continues gaining traction. This raises a practical question. When Claude Code is given free rein, what does it actually pick, and how repeatable are those preferences? The answer has real consequences for teams delegating scaffolding decisions to an agent.

Note: The observations in this article are based on informal testing, not a peer-reviewed study. All frequency figures are approximate and reflect a limited number of trials. Your results may differ depending on Claude Code version, model updates, and prompt phrasing. Where results figures appear later in this article, this same caveat applies.

Study Methodology — How We Tested Claude Code's Decision-Making

The Prompt Design

The approach centered on open-ended prompts deliberately free of tool constraints. Prompts like "Build a full-stack task manager" or "Create a real-time chat application" let Claude Code make every tooling decision independently. Testing spanned multiple categories: frontend framework, backend runtime and framework, database, package manager, and deployment target. We ran 10 trials per prompt category to establish frequency patterns rather than relying on single-run anecdotes.

Controlled Variables and Environment

Tests used Claude Code with default settings and no custom instructions, no CLAUDE.md files, and no prior conversation context that might bias selections. We categorized results by logging which specific tools, frameworks, and configurations appeared in the generated output across each trial.

Prerequisites for reproducing this methodology:

  • Claude Code CLI installed and authenticated — confirm with claude --version
  • Node.js ≥ 20.0.0 (Node 18 reached end-of-life in April 2025)
  • npm ≥ 7.0.0 (required for workspaces)
  • OS: Linux or macOS (the script uses bash arrays and process substitution — not Windows-compatible without modification)
  • Shell: bash ≥ 3.2
  • Active Claude API key with sufficient credits for all invocations
  • jq installed for reliable JSON parsing — confirm with jq --version
  • Confirm the CLI flags used below exist in your installed version by running claude --help

The following shell script demonstrates how to reproduce this methodology systematically:

#!/usr/bin/env bash
# shellcheck shell=bash
set -euo pipefail

# Requires bash >= 3.2 on macOS or Linux.
# Verify that your installed Claude Code CLI supports the flags below
# by running: claude --help
# If --print or --output-format are not listed, consult the Claude Code
# documentation for the correct invocation syntax.

# --- Prerequisite checks ---
command -v claude >/dev/null 2>&1 || { echo "ERROR: claude CLI not found"; exit 1; }
command -v jq    >/dev/null 2>&1 || { echo "ERROR: jq not found — required for JSON parsing"; exit 1; }

node_version=$(node --version 2>/dev/null | tr -d 'v' | cut -d. -f1)
if [[ -z "$node_version" || "$node_version" -lt 20 ]]; then
  echo "ERROR: Node.js >= 20.0.0 required. Found: $(node --version 2>/dev/null || echo 'none')"
  exit 1
fi

npm_version=$(npm --version 2>/dev/null | cut -d. -f1)
if [[ -z "$npm_version" || "$npm_version" -lt 7 ]]; then
  echo "ERROR: npm >= 7.0.0 required for workspace support. Found: $(npm --version 2>/dev/null || echo 'none')"
  exit 1
fi

# --- Prompt definitions ---
# IMPORTANT: All prompts are defined as static literals below. Do NOT
# populate this array from files, environment variables, or user input
# without additional sanitization — doing so risks shell injection.
PROMPTS=(
  "Build a full-stack task manager with user authentication"
  "Create a real-time chat application with rooms"
  "Build an e-commerce product catalog with search"
  "Create a blog platform with comments and tags"
  "Build a project management dashboard with charts"
)

# --- Run-scoped log directory ---
RUN_ID="$(date +%Y%m%dT%H%M%S)_$$"
LOGDIR="./claude-tool-audit/run_${RUN_ID}"
mkdir -p "$LOGDIR"
echo "Logging to: $LOGDIR"

# --- Confirmation gate ---
echo "Warning: This script runs ${#PROMPTS[@]} Claude Code invocations."
echo "Verify API credit availability before proceeding."
read -r -p "Press Enter to continue or Ctrl+C to abort..." _

# --- Keywords for analysis ---
KEYWORDS=("react" "vue" "svelte" "angular" "express" "fastify" "next"
          "postgres" "mysql" "mongo" "npm" "yarn" "pnpm" "aws" "vercel" "docker")

# --- Helper function for running a single trial ---
run_claude_trial() {
  local prompt="$1"
  local logfile="$2"
  local errfile="$3"
  # Prompt is passed as a single argument — no word splitting occurs.
  claude --print --output-format json "$prompt" > "$logfile" 2>"$errfile"
}

# --- Main trial loop ---
for i in "${!PROMPTS[@]}"; do
  PROMPT="${PROMPTS[$i]}"
  TIMESTAMP="$(date +%Y%m%dT%H%M%S)_$$"
  LOGFILE="$LOGDIR/trial_$(printf '%04d' "$i")_${TIMESTAMP}.json"
  ERRFILE="$LOGDIR/errors_$(printf '%04d' "$i")_${TIMESTAMP}.log"
  TMPFILE="${LOGFILE}.tmp"

  echo "Running trial $i: $PROMPT"

  # NOTE: Verify that --print and --output-format json are valid flags
  # in your Claude Code version. Run `claude --help` to confirm.
  if run_claude_trial "$PROMPT" "$TMPFILE" "$ERRFILE"; then
    # Validate JSON before promoting to final log location
    if jq empty "$TMPFILE" 2>>"$ERRFILE"; then
      mv "$TMPFILE" "$LOGFILE"
    else
      echo "Trial $i: Output is not valid JSON. Saved raw output to ${LOGFILE}.invalid"
      mv "$TMPFILE" "${LOGFILE}.invalid"
      continue
    fi
  else
    EXIT_CODE=$?
    echo "Trial $i failed (exit code $EXIT_CODE). See $ERRFILE for details."
    rm -f "$TMPFILE"
    continue
  fi

  # --- jq-based keyword extraction ---
  echo "--- Trial $i Tool Selections (jq parse) ---"

  # Extract text content from JSON output field before keyword matching.
  # Adjust '.output' to match the actual field name in claude CLI JSON response.
  OUTPUT_TEXT=$(jq -r '.output // .text // .content // empty' "$LOGFILE" 2>/dev/null)

  if [[ -z "$OUTPUT_TEXT" ]]; then
    echo "  WARNING: Could not extract text field from JSON. Check field name with: jq 'keys' $LOGFILE"
  else
    for kw in "${KEYWORDS[@]}"; do
      count=$(echo "$OUTPUT_TEXT" | grep -ioE "\b${kw}\b" | wc -l | tr -d ' ')
      if [[ "$count" -gt 0 ]]; then
        echo "  $kw: $count mention(s)"
      fi
    done
  fi
  echo ""
done

echo "Audit complete. Results logged to $LOGDIR/"
echo "Review $LOGDIR/errors_*.log for any failed trials."

This script fires neutral prompts through the Claude Code CLI, logs full output to a run-scoped directory, validates JSON integrity before saving, and performs keyword extraction using jq to parse the JSON output field before matching. Running it across sessions with fresh context produces the frequency data needed for analysis.

The Results — What Claude Code Actually Picks

Frontend Framework Preferences

React dominated. Across open-ended full-stack prompts, Claude Code picked React in ~85% of trials (N=10 per category; see methodology for caveats). Vue showed up in ~8% of trials; Svelte appeared only when prompt wording hinted at simplicity or lightweight requirements. Next.js appeared frequently as the React meta-framework of choice, further concentrating the React ecosystem's representation.

Backend and Database Choices

On the backend, Node.js with Express appeared in ~80% of trials. Claude Code chose PostgreSQL over MySQL or MongoDB in most database decisions, particularly for relational data models. For package management, npm was the default in ~90% of trials. Yarn appeared in ~7% of cases; pnpm never appeared in default outputs.

Claude Code ignores tools like SQLite and pnpm unless explicitly told to use them.

Infrastructure and Deployment Patterns

Claude Code defaulted to AWS — typically EC2 or Lambda with RDS — in ~65% of trials. Project structures followed conventional conventions: separate /client and /server directories, standard middleware patterns, and environment variable configuration via .env files (ensure .env is listed in .gitignore to prevent secret exposure).

Claude Code Tool Selection Frequency by Category:

CategoryPrimary ChoicePrimary Freq.Secondary ChoiceSecondary Freq.
FrontendReact / Next.js~85%Vue~8%
BackendNode.js / Express~80%Python / FastAPI~10%
DatabasePostgreSQL~70%MongoDB~18%
Package Managernpm~90%yarn~7%
Cloud ProviderAWS~65%Vercel~20%

Based on N=10 trials per category. Results are descriptive only. Remaining percentage in each row reflects other choices, inconclusive outputs, or cases where no clear selection was observed.

Below is a representative scaffold that Claude Code typically generates for a full-stack prompt. Annotations follow the JSON block.

{
  "name": "fullstack-task-manager",
  "private": true,
  "workspaces": ["client", "server"],
  "scripts": {
    "dev": "concurrently \"npm run dev --workspace=server\" \"npm run dev --workspace=client\"",
    "build": "npm run build --workspace=client && npm run build --workspace=server",
    "start": "npm run start --workspace=server"
  },
  "devDependencies": {
    "concurrently": "8.2.0"
  },
  "engines": {
    "node": ">=20.0.0",
    "npm": ">=7.0.0"
  }
}

The scaffold uses React 18 via Vite or Next.js for the frontend (client/); Create React App is deprecated as of 2023 and should not be used for new projects. The backend (server/) runs Express 4.x with the pg driver for PostgreSQL, and schema management happens through raw SQL or Prisma. npm workspaces handle dependency management (yarn and pnpm were not selected by default in observed trials). Deployment targets AWS: a Dockerfile is included, with references to ECR/ECS or Lambda. The --workspace flag in scripts requires npm ≥ 7, and the engines field enforces both Node and npm minimum versions — verify yours with npm --version. Note that concurrently is pinned to an exact version (8.2.0, no ^ prefix) to prevent silent breaking upgrades.

What This Means for Your Workflow

The Tradeoff: Predictable Defaults vs. Invisible Lock-in

Predictable stacks have genuine advantages. During rapid prototyping, hackathons, or greenfield projects, Claude Code's consistent selections mean every team member generates compatible scaffolding without coordination overhead. The React/Express/PostgreSQL combination has extensive documentation and broad community support.

But the risk is invisible tech stack lock-in. Teams working with existing Vue or Angular codebases may find Claude Code generating incompatible scaffolding. Overrepresentation of popular tools means potentially better-fit alternatives like SQLite for embedded use cases, or pnpm for monorepo performance, get systematically overlooked. Claude Code ignores tools like SQLite and pnpm unless explicitly told to use them.

The risk is invisible tech stack lock-in. Teams working with existing Vue or Angular codebases may find Claude Code generating incompatible scaffolding.

How to Override Preferences

The CLAUDE.md file at the project root is the primary mechanism for directing Claude Code's decisions. Place CLAUDE.md in the directory from which you invoke claude, or the root of the repository Claude Code is given access to. Here is an example that overrides defaults:

# CLAUDE.md

## Tech Stack Requirements
- Frontend: Vue 3 with Composition API (NOT React)
- Backend: Python with FastAPI
- Database: MySQL 8.x
- Package Manager: pnpm (do not use npm or yarn)
- Deployment: Google Cloud Run with Cloud SQL
- Testing: Vitest for frontend, pytest for backend

## Conventions
- Use TypeScript for all Vue components
- Follow PEP 8 for Python code
- All API endpoints must include OpenAPI docstrings

With this file present, a prompt like "Build a full-stack task manager" produced Vue/FastAPI/MySQL output in 8 of 10 trials. Compliance is not guaranteed; LLM behavior is non-deterministic, so verify outputs before treating them as correct. Without a CLAUDE.md file, the same prompt reliably gravitates toward default selections.

Implementation Checklist — Audit Your AI-Assisted Projects

  1. Review Claude Code's default selections in your last three projects and note which tools it chose without explicit instruction. Compare those selections against your team's preferred or mandated stack.
  2. Create a CLAUDE.md file with explicit tool, framework, and configuration requirements.
  3. Test with a neutral prompt (no tool mentions) to verify the overrides take effect. Run the test multiple times — a single pass is not sufficient given non-deterministic behavior.
  4. Document any deviations where Claude Code ignored or partially followed CLAUDE.md directives.
  5. Review and update preferences quarterly as tooling evolves. Check Claude Code release notes for model updates that may shift default behaviors.
  6. Share CLAUDE.md files across team repositories to maintain consistency.

Verifying the Script

The following tests validate key behaviors of the audit script. Run them to confirm correctness in your environment.

Unit Tests

#!/usr/bin/env bash
# test_audit_script.sh — run with: bash test_audit_script.sh

PASS=0; FAIL=0

assert_eq() {
  local desc="$1" expected="$2" actual="$3"
  if [[ "$expected" == "$actual" ]]; then
    echo "  PASS: $desc"; ((PASS++))
  else
    echo "  FAIL: $desc — expected '$expected', got '$actual'"; ((FAIL++))
  fi
}

# Test 1: Timestamp+PID uniqueness — PIDs differ across subshells
ts_sub1=$(bash -c 'echo "$(date +%Y%m%dT%H%M%S)_$$"')
ts_sub2=$(bash -c 'echo "$(date +%Y%m%dT%H%M%S)_$$"')
assert_eq "timestamp_pid_differs_across_subshells" "1" "$([[ "$ts_sub1" != "$ts_sub2" ]] && echo 1 || echo 0)"

# Test 2: Prerequisite check exits non-zero when tool is absent
result=$(bash -c '
  command -v jq_nonexistent_binary >/dev/null 2>&1 || { echo "MISSING"; exit 1; }
' 2>/dev/null; echo $?)
assert_eq "prerequisite_check_exits_on_missing_tool" "1" "$result"

# Test 3: JSON validation guard — invalid JSON is detected
TMPDIR_TEST=$(mktemp -d)
echo "not valid json" > "$TMPDIR_TEST/test.json.tmp"
if jq empty "$TMPDIR_TEST/test.json.tmp" 2>/dev/null; then
  assert_eq "invalid_json_detected" "INVALID" "VALID"
else
  assert_eq "invalid_json_detected" "1" "1"
fi
rm -rf "$TMPDIR_TEST"

# Test 4: Keyword extraction — word boundary matching
OUTPUT='{"output": "See https://react-express.com for docs"}'
count=$(echo "$OUTPUT" | jq -r '.output' | grep -ioE '\breact\b' | wc -l | tr -d ' ')
assert_eq "keyword_extraction_word_boundary_react" "1" "$count"

# Test 5: engines field includes npm version constraint
PKG_JSON='{"engines": {"node": ">=20.0.0", "npm": ">=7.0.0"}}'
npm_engine=$(echo "$PKG_JSON" | jq -r '.engines.npm // empty')
assert_eq "package_json_has_npm_engine_constraint" ">=7.0.0" "$npm_engine"

echo ""
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]]

Integration Test

#!/usr/bin/env bash
# integration_test.sh — validates full script execution against a mock claude command
set -euo pipefail

TMPDIR_IT=$(mktemp -d)
trap 'rm -rf "$TMPDIR_IT"' EXIT

# Create mock claude binary
cat > "$TMPDIR_IT/claude" <<'EOF'
#!/usr/bin/env bash
# Mock claude CLI — outputs valid JSON with known content
echo '{"output": "This project uses React with Express and PostgreSQL via npm workspaces"}'
exit 0
EOF
chmod +x "$TMPDIR_IT/claude"

export PATH="$TMPDIR_IT:$PATH"

LOGDIR="$TMPDIR_IT/logs/run_test"
mkdir -p "$LOGDIR"

KEYWORDS=("react" "express" "postgres")
PROMPTS=("Build a full-stack task manager")

for i in "${!PROMPTS[@]}"; do
  PROMPT="${PROMPTS[$i]}"
  TIMESTAMP="$(date +%Y%m%dT%H%M%S)_$$_${i}"
  LOGFILE="$LOGDIR/trial_$(printf '%04d' "$i")_${TIMESTAMP}.json"
  ERRFILE="$LOGDIR/errors_$(printf '%04d' "$i")_${TIMESTAMP}.log"
  TMPFILE="${LOGFILE}.tmp"

  if claude --print --output-format json "$PROMPT" > "$TMPFILE" 2>"$ERRFILE"; then
    if jq empty "$TMPFILE" 2>>"$ERRFILE"; then
      mv "$TMPFILE" "$LOGFILE"
    else
      mv "$TMPFILE" "${LOGFILE}.invalid"
    fi
  fi

  [[ -f "$LOGFILE" ]] || { echo "FAIL: Log file not created"; exit 1; }

  count=$(jq -r '.output' "$LOGFILE" | grep -ioE '\breact\b' | wc -l | tr -d ' ')
  [[ "$count" -ge 1 ]] || { echo "FAIL: Expected 'react' in output, found $count"; exit 1; }
done

echo "PASS: Integration test completed — log created and keyword detected correctly"

Sanity Check

# Verify JSON output field is parseable and keyword extraction works correctly
echo '{"output": "Using React with Express and PostgreSQL"}' | \
  jq -r '.output' | \
  grep -ioE '\b(react|express|postgres)\b' | \
  sort | uniq -c | sort -rn

Expected output:

      1 react
      1 postgres
      1 express

Key Takeaways

Claude Code favors React, Express, PostgreSQL, npm, and AWS when given open-ended prompts. These preferences are consistent across trials but fully overridable with CLAUDE.md instructions. Run your own audits using the provided scripts, and verify CLI flag syntax against your installed version before executing.