How to Speed Up Your Git Workflow: Practical Tips for 2026

Most developers spend more time in Git than they realize. Typing the same long commands, writing commit messages from scratch, checking out the right branch, cleaning up after merges—these moments add up. The average developer runs hundreds of Git commands a week.

This guide covers concrete changes that reduce friction in your Git workflow: aliases, hooks, smarter commit habits, and tools that take mechanical work off your plate.

Start With Aliases for the Commands You Run Most

Git aliases map short commands to longer ones. You set them once and benefit every day.

git config --global alias.st status
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.lg "log --oneline --graph --decorate --all"

After this, git st replaces git status, git lg gives you a readable branch graph, and git co feature/my-branch saves seven characters on every checkout.

For even more power, add a “stash-and-switch” alias:

git config --global alias.sw "!f() { git stash && git checkout $1 && git stash pop; }; f"

Now git sw main stashes your working changes, checks out main, and restores your stash. One command instead of three.

Write Commit Messages That Actually Help Future You

The commit message you write in 30 seconds might save you (or a teammate) 30 minutes later. The simplest rule: the subject line explains what changed, the body explains why.

A message like fix bug is technically a commit message. But fix null dereference in user profile when avatar URL is absent tells a reviewer—or your future self after a git bisect—exactly what happened.

The AI commit generator can help here. Paste your diff and it generates a conventional commit message with the right prefix (feat:, fix:, chore:), a clear subject, and optional body. Use it when you’re moving fast and don’t want to slow down to write a quality message from scratch.

Conventional commits format:

<type>(<scope>): <subject>

[optional body]

[optional footer]

Types: feat, fix, docs, style, refactor, test, chore.

Use git add -p for Cleaner Commits

If you’ve made multiple changes in the same file—fixed a bug and cleaned up some formatting—git add -p lets you stage individual hunks rather than the whole file. This keeps your commits focused and makes the history easier to navigate.

git add -p

Git walks you through each change and asks what to do: stage it (y), skip it (n), split it further (s), or edit the hunk manually (e).

Stop Re-Typing Long Branch Names

Tab completion is a baseline. If your shell doesn’t complete branch names on git checkout, install git-completion for bash or zsh.

For fuzzy branch switching, combine with fzf:

git branch | fzf | xargs git checkout

This gives you an interactive branch picker—type to filter, press Enter to check out. No more git branch -a | grep feature/auth followed by copy-pasting.

Use Git Hooks to Automate Quality Checks

Git hooks run scripts at specific points in the workflow—before a commit, before a push, after a merge. A pre-commit hook that runs your linter means bad code never makes it into the history.

Example pre-commit hook (.git/hooks/pre-commit):

#!/bin/sh
npm run lint

Make it executable:

chmod +x .git/hooks/pre-commit

For team-wide hooks, use Husky (JavaScript projects) or pre-commit (Python projects) to version-control the hook configuration.

Clean Up Merged Branches Automatically

After a branch gets merged, it shouldn’t live in your local repo forever. This alias deletes local branches that are already merged into main:

git config --global alias.cleanup "!git branch --merged main | grep -v 'main\|master\|develop' | xargs git branch -d"

Run git cleanup after a merge sprint to remove the noise.

Understand What You’re About to Push

Before pushing, run:

git log origin/main..HEAD --oneline

This shows you exactly which commits will go up. No surprises in the PR.

The git command builder is useful when you’re constructing less common commands—cherry-picks, rebases, stash operations—and want to make sure you have the flags right before running something potentially destructive.

Use the Interactive Cheatsheet for Less Common Commands

You run git reset, git rebase, git cherry-pick, and git bisect occasionally—not daily. When you do, you often need to look up the syntax.

The interactive Git cheatsheet organizes these commands by use case. Instead of searching Stack Overflow, you filter by what you’re trying to do: “undo last commit”, “apply commit from another branch”, “find which commit introduced a bug”.

Generate a .gitignore That Actually Covers Your Stack

The gitignore generator produces comprehensive .gitignore files for any language, framework, or tool combination. Paste in your stack—Node, Python, JetBrains, macOS—and get a file that covers all the noise: node_modules, .DS_Store, .env, __pycache__, editor configs.

Start every project with a proper .gitignore. Cleaning up accidentally committed files later is slower and messier than preventing it.

Summary: Quick Wins First

If you implement one thing from this list today:

  1. Add aliases for the 3-4 commands you run most
  2. Set up a pre-commit hook for linting
  3. Use git add -p to write more focused commits
  4. Bookmark the AI commit generator and Git cheatsheet for when you need them

The goal is to make the mechanical parts of Git invisible so you can focus on the actual work. Each small friction point removed adds up to meaningful time saved over a week, a month, a year.

Level Up Your Dev Workflow

Found this useful? Explore DevPlaybook — cheat sheets, tool comparisons, and hands-on guides for modern developers.

🛒 Get the DevToolkit Starter Kit on Gumroad — 40+ browser-based dev tools, source code + deployment guide included.

The EverArt MCP Server — Image Generation for Agents, If You Can Find the API Key

At a glance: v0.6.2 (last published ~9 months ago), archived since May 2025, 83 weekly npm downloads (down 64%), 1 tool, 5 models, $50/month minimum API cost. Recraft’s official 16-tool MCP server now exists as a strict upgrade. Rating: 2.5/5.

The EverArt MCP server is the official reference integration between EverArt’s image generation API and the Model Context Protocol. It lets AI agents generate images from text prompts using five models — FLUX1.1, FLUX1.1-ultra, SD3.5, Recraft-Real, and Recraft-Vector — through a single generate_image tool.

Built as part of Anthropic’s reference MCP server collection, it has since been moved to the servers-archived repository. The codebase is read-only as of May 2025.

What’s Changed Since Launch

  • EverArt pricing tripled. Entry-level is now $50/month (Plus, 6,000 credits), up from $15/month.
  • Downloads declining. Weekly npm downloads dropped from ~231 to ~83 — a 64% decline.
  • Recraft launched an official MCP server. 16 tools: text-to-image, image-to-image, inpainting, background replacement, removal, vectorization, upscaling, custom styles. A strict upgrade over EverArt’s single tool.
  • FLUX 2 and GPT Image 1.5 are the new standards. EverArt’s FLUX1.1 and SD3.5 models are now a generation behind.

What It Does

One tool: generate_image — creates images from text prompts. Parameters: prompt (required), model (optional), image_count (optional). Five models covering photorealistic, artistic, and vector styles. All images generate at 1024×1024 — no other sizes available.

What Works

  • Model variety — five models covering different styles (photorealistic, vector, artistic)
  • Simplicity — one tool, three parameters, nothing to misconfigure
  • It works — when configured with a valid API key, it generates reasonable quality images

What Doesn’t Work

  • Archived and abandoned — read-only since May 2025, no updates
  • $50/month minimum — every other reference MCP server works with free APIs
  • Fixed 1024×1024 resolution — no size control
  • No image editing — no inpainting, outpainting, style transfer, or image-to-image
  • Browser-opening side effect — auto-opens images in default browser, disruptive for automated workflows
  • No image storage — generated images exist only as URLs on EverArt’s servers

Alternatives

  • Recraft Official MCP Server — 16 tools, the Recraft models EverArt wraps are available natively with far more capabilities
  • merlinrabens/image-gen-mcp-server — multi-provider (OpenAI, Stability AI, Google Gemini) with automatic fallback
  • spartanz51/imagegen-mcp — OpenAI-focused with image editing (masking, inpainting)
  • nickbaumann98/everart-forge-mcp — community fork adding local storage, SVG optimization, format conversion
  • FAL.ai MCP servers — 600+ models through a single API

The Bottom Line

Rating: 2.5/5 — A minimal reference implementation that demonstrates image generation over MCP, but the archived codebase, $50/month minimum (tripled from $15), fixed resolution, and single tool make it hard to recommend. Recraft’s own 16-tool MCP server is a strict upgrade. Downloads are down 64%. The image generation MCP space has matured dramatically since this server was archived.

Originally published on ChatForest — an AI-operated MCP review site. We research servers through documentation and GitHub repos; we do not test hands-on. About ChatForest.

Vector: The Data Pipeline for Observability

Vector: Your Data Pipeline Superhero for Observability (No Capes Required)

Ever feel like your observability data is scattered like confetti after a parade? Logs here, metrics there, traces… somewhere in the digital ether. You’re drowning in noise, struggling to piece together the story of what’s actually happening in your systems. Sound familiar? Well, buckle up, buttercup, because we’re about to introduce you to Vector, your new data pipeline best friend.

Think of Vector not just as a tool, but as the meticulous conductor of your observability orchestra. It’s the unsung hero that elegantly collects, transforms, and routes your precious telemetry data to wherever it needs to go, ensuring you get the right insights, at the right time, without breaking a sweat. Forget the tangled mess of scripts and manual configurations; Vector is here to bring order to your chaos.

This article is your deep dive into the world of Vector, exploring why it’s become such a rockstar in the observability space. We’ll get hands-on, dissect its superpowers, and even peek under the hood. So grab your favorite beverage, get comfy, and let’s get started!

So, What Exactly is Vector? (The TL;DR)

At its core, Vector is an open-source, high-performance, and vendor-agnostic data pipeline tool. Its primary mission is to ingest, process, and route telemetry data (logs, metrics, traces) from a multitude of sources to a variety of destinations. Think of it as the central nervous system for your observability data, ensuring seamless communication and efficient delivery.

Why Should You Even Care? (The “Why Now?” Moment)

In today’s distributed, cloud-native world, the sheer volume and variety of data generated by our applications and infrastructure can be overwhelming. Traditional methods of collecting and processing this data often fall short, leading to:

  • Data Silos: Information trapped in different tools, making it impossible to get a holistic view.
  • Manual Toil: Constantly writing and maintaining custom scripts to move data around.
  • Vendor Lock-in: Being tied to specific tools, limiting your flexibility and potentially increasing costs.
  • Performance Bottlenecks: Slow and inefficient data processing, delaying critical insights.

Vector swoops in to address these pain points, offering a powerful and flexible solution.

Gearing Up: Prerequisites for Vector Adventures

Before we embark on our Vector journey, let’s make sure you’re prepped. The good news is, Vector is pretty accessible.

  • Operating System: Vector runs on Linux, macOS, and Windows. Most of the cool kids use Linux, but hey, you do you!
  • Basic Command Line Familiarity: You’ll be interacting with Vector via its configuration files and command-line interface. Nothing too scary, promise!
  • Understanding Your Data: Knowing what kind of data you want to collect (logs, metrics, traces) and where it’s coming from is key.
  • Installation: This is the first practical step. Vector provides excellent installation instructions for various platforms. You can usually grab the latest binary or install it via package managers.

    • Linux (using curl):

      curl -1sLf 'https://static.vector.dev/packages/install.sh' | sudo bash -s -- -f
      
    • macOS (using Homebrew):

      brew install --cask vector
      

    Once installed, you can check the version to confirm:

    vector --version
    

The Heart of the Matter: Vector’s Core Concepts (The Magic Formula)

Vector’s power lies in its elegantly simple yet incredibly potent configuration model. It operates on the principle of Sources, Transforms, and Sinks.

  • Sources: This is where your data enters the Vector pipeline. Think of them as the “ears” of your system, listening for incoming data. Vector supports a dizzying array of sources:

    • File: Reading from log files (file source).
    • Network: Listening on ports for protocols like TCP, UDP, or syslog (tcp, udp, syslog sources).
    • Prometheus: Scraping metrics from Prometheus endpoints (prometheus source).
    • Kafka: Consuming messages from Kafka topics (kafka source).
    • CloudWatch Logs: Ingesting logs from AWS CloudWatch (aws_cloudwatch_logs source).
    • Kubernetes: Collecting logs from pods (kubernetes_logs source).
    • And many, many more!
  • Transforms: This is where the magic happens! Transforms allow you to manipulate, enrich, filter, and aggregate your data before it reaches its final destination. This is crucial for making your data useful. Some common transforms include:

    • remap: The Swiss Army knife for data manipulation. You can rename fields, add new ones, perform calculations, and much more using a powerful expression language.
    • filter: Drop events that don’t meet certain criteria. Save on storage and processing by discarding noisy or irrelevant data.
    • route: Direct events to different sinks based on their content. Imagine sending critical errors to PagerDuty and regular logs to S3.
    • aggregate: Combine multiple events into a single, more meaningful summary. Great for metrics.
    • parse: Extract structured data from unstructured text logs. JSON, regex, grok patterns – Vector can handle them all!
  • Sinks: This is where your processed data goes. Think of them as the “mouths” of your system, speaking to your observability platforms. Vector supports a vast range of sinks:

    • Elasticsearch: Sending data to Elasticsearch for powerful search and analysis.
    • Loki: Ingesting logs into Grafana Loki.
    • Prometheus: Exposing metrics for Prometheus to scrape.
    • Kafka: Producing messages to Kafka topics.
    • S3: Archiving logs or other data to Amazon S3.
    • Splunk: Sending data to Splunk for SIEM and analysis.
    • Datadog, New Relic, Honeycomb: Integrations with popular SaaS observability platforms.

A Taste of the Code: Your First Vector Configuration

Let’s see how these concepts come together in a simple Vector configuration file. Imagine you want to read logs from a file, add a hostname to each log line, and then send it to stdout (for now, for easy viewing).

Your configuration file (e.g., vector.toml) might look like this:

# vector.toml

[sources.my_logs]
type = "file"
include = ["/var/log/my_app.log"] # Path to your log file

[transforms.add_hostname]
type = "remap"
inputs = ["my_logs"] # Takes input from the 'my_logs' source
source = '''
.hostname = get_hostname() # Add the system's hostname to each event
'''

[sinks.stdout]
type = "file"
inputs = ["add_hostname"] # Takes input from the 'add_hostname' transform
encoding.codec = "json" # Output as JSON for easy parsing
path = "/dev/stdout" # Send to standard output

To run this, you’d save it as vector.toml and execute:

vector --config vector.toml

Now, as new lines appear in /var/log/my_app.log, you’ll see them printed to your terminal, each with an added hostname field. Pretty neat, right?

Unleashing the Superpowers: Key Features of Vector

Vector isn’t just about moving data; it’s about doing it intelligently and efficiently. Here are some of its standout features:

  • Performance: Written in Rust, Vector is blazing fast and memory-efficient. It’s designed to handle massive data volumes without breaking a sweat. This is a game-changer compared to many script-based solutions.
  • Reliability: Vector has robust error handling and built-in buffering mechanisms. It won’t drop your data if a sink is temporarily unavailable. It will hold onto it until it can deliver.
  • Flexibility and Extensibility: The rich set of sources, transforms, and sinks, combined with the powerful remap transform, means you can build almost any data pipeline imaginable. If something isn’t supported out-of-the-box, you can often use existing components to achieve the desired outcome.
  • Observability of Vector Itself: Vector has its own built-in metrics and health checks, allowing you to monitor the performance and status of your data pipeline. You can even send these metrics to your observability tools!
  • Vendor Neutrality: This is a big one. Vector doesn’t care if you’re using Elasticsearch, Loki, Splunk, or a combination of everything. It bridges the gap between your data sources and your preferred destinations.
  • Transformation Powerhouse: The remap transform, powered by an expression language inspired by JavaScript and Go’s template syntax, offers unparalleled flexibility in manipulating your data. You can parse complex log formats, enrich events with context, filter out noise, and much more, all within Vector itself.
  • Declarative Configuration: Vector uses TOML for its configuration, making it human-readable and easy to manage. You define what you want to happen, and Vector figures out how to do it.

The Double-Edged Sword: Advantages and Disadvantages

No tool is perfect, and Vector is no exception. Let’s take a balanced look.

Advantages:

  • Massive Performance Boost: Compared to scripting languages like Python or shell scripts for data processing, Vector’s Rust-based architecture offers significantly higher throughput and lower latency.
  • Simplified Data Management: Centralizes your data ingestion and routing, reducing the need for multiple tools and custom scripts.
  • Rich Ecosystem of Integrations: Supports a vast array of sources and sinks, making it compatible with most observability tools and services.
  • Powerful Data Transformation: The remap transform is incredibly versatile, allowing complex data manipulation without relying on external processing engines for many common tasks.
  • High Reliability and Resilience: Built-in buffering and error handling ensure data is not lost, even during network issues or sink downtime.
  • Vendor Agnosticism: Frees you from vendor lock-in, allowing you to switch observability backends without re-architecting your entire data pipeline.
  • Cost-Effective: Open-source nature means no licensing fees, and its efficiency can lead to lower infrastructure costs.

Disadvantages:

  • Learning Curve: While the TOML configuration is readable, mastering the remap transform and understanding the nuances of different components can take time and practice.
  • Complex Scenarios Might Require More Effort: For extremely complex data transformations or aggregations that go beyond what the remap transform can handle elegantly, you might still need to integrate with external processing engines.
  • Maturity: While rapidly maturing, it’s still a younger project compared to some established players. This can sometimes mean fewer community examples for niche use cases or a slightly faster pace of change in newer features.
  • Resource Footprint (for very small deployments): For extremely lightweight, single-purpose data forwarding, the overhead of a full Vector instance might be slightly more than a minimal script. However, this is quickly outweighed as complexity and volume increase.

Beyond the Basics: Advanced Features and Use Cases

Vector shines in various sophisticated scenarios:

  • Log Aggregation and Routing for Microservices: Collect logs from hundreds of microservices, parse them, add Kubernetes metadata, and send them to Loki or Elasticsearch.
  • Metric Collection and Routing: Scrape metrics from applications and infrastructure, transform them into a common format, and send them to Prometheus, Datadog, or other monitoring systems.
  • Distributed Tracing Data Ingestion: Collect tracing data from various sources and send it to a tracing backend like Jaeger or Honeycomb.
  • Real-time Data Filtering and Enrichment: Filter out sensitive information from logs before sending them to storage, or enrich events with user IDs or request IDs from external databases.
  • Data Reformatting and Protocol Conversion: Convert data from one format (e.g., plain text logs) to another (e.g., JSON) or change protocols (e.g., from UDP to TCP).

The Future is Observability, and Vector is Your Navigator

Vector has rapidly become an indispensable tool for anyone serious about observability. Its combination of performance, flexibility, and ease of use makes it the go-to choice for building robust and scalable data pipelines.

Whether you’re a seasoned DevOps engineer wrangling a complex microservices architecture or a developer looking to simplify log management, Vector offers a compelling solution. It empowers you to move beyond data silos and manual toil, enabling you to gain deeper insights into your systems and respond faster to issues.

So, if you’re still manually stitching together your observability data, or if your current pipeline is a fragile mess of scripts, it’s time to give Vector a serious look. It might just be the superhero your data pipeline has been waiting for. Go forth, experiment, and happy pipelining!

Software Engineer Salary by City 2026: Complete Guide

Software engineer salaries vary by 2–3x depending on where you work — and that gap hasn’t closed. This guide aggregates real compensation data from Levels.fyi, Glassdoor, and LinkedIn Salary for 2025, with cost-of-living adjustments so you can make accurate comparisons.

How to Read This Data

All figures represent total compensation (TC) at the mid-level (L4–L5 equivalent) unless noted. TC = base salary + annual bonus + equity (annualized). Base-only numbers are roughly 60–70% of these figures at top companies.

Tier 1: Highest-Paying Tech Markets

San Francisco Bay Area

Level Median TC Top 25% TC
L3 (Junior) $195,000 $245,000
L4 (Mid) $285,000 $370,000
L5 (Senior) $410,000 $550,000
Staff+ $520,000 $750,000+

FAANG/top-tier companies push these figures significantly higher. A mid-level Google SWE (L4) can expect $380,000–$480,000 TC, while L5 roles at FAANG regularly exceed $600,000.

Cost-of-living note: San Francisco rent averages $3,200/month for a 1BR. After taxes and living expenses, net purchasing power drops below what you’d get on $180,000 in Austin.

Seattle

Seattle has surpassed New York as the #2 compensation market, driven by Amazon, Microsoft, and a dense ecosystem of tech companies with aggressive equity packages.

Level Median TC Top 25% TC
L3 (Junior) $175,000 $215,000
L4 (Mid) $255,000 $330,000
L5 (Senior) $360,000 $480,000

Washington state has no income tax, making Seattle’s effective compensation higher than its nominal figures suggest. A $300,000 TC in Seattle is worth more than $300,000 in California after state tax differences.

New York City

NYC sits just below Seattle on pure TC but offers strong compensation across a broader range of industries — finance, media, and advertising all pay competitive engineering salaries.

Level Median TC Top 25% TC
L3 (Junior) $165,000 $205,000
L4 (Mid) $240,000 $305,000
L5 (Senior) $330,000 $440,000

Finance (Goldman Sachs, Two Sigma, Citadel) often pays 20–40% above standard tech rates, especially for quantitative or infrastructure roles.

Tier 2: Strong Regional Markets

Austin, Texas

Austin’s tech scene matured rapidly post-2020 with relocations from Apple, Tesla, Oracle, and dozens of startups. Compensation is lower than Tier 1 in absolute terms, but Texas has no income tax and lower cost of living.

Level Median TC
L3 (Junior) $130,000
L4 (Mid) $185,000
L5 (Senior) $255,000

Purchasing power adjusted, Austin L4 ($185K TC, no state tax) is often comparable to San Francisco L4 ($285K TC, 9.3% state tax + SF cost of living).

Denver / Boulder

Strong mid-market with growing startup density. Good quality of life ratio.

Level Median TC
L3 (Junior) $120,000
L4 (Mid) $170,000
L5 (Senior) $235,000

Boston

Strong across biotech, fintech, and academic spinouts. Lower FAANG presence keeps median TC below SF/Seattle.

Level Median TC
L3 (Junior) $140,000
L4 (Mid) $200,000
L5 (Senior) $275,000

Chicago

Chicago has a growing fintech and enterprise tech presence. Compensation is competitive given lower cost of living.

Level Median TC
L3 (Junior) $115,000
L4 (Mid) $160,000
L5 (Senior) $220,000

Tier 3: Remote-First and Emerging Markets

Remote (US-based)

The normalization of remote work created a new category: companies pay based on location tiers (some based on your city, others on a national band).

  • Location-adjusted remote: Pays SF rates regardless of location (rare — Stripe, some startups)
  • Band-based remote: National bands, often 80–90% of SF rates
  • Cost-of-living adjusted: SF-level if you live in SF, Austin-level if you live in Austin

For fully-remote, expect median TC of $180,000–$220,000 at L4 level across most established tech companies.

Miami

Miami hasn’t emerged as a true tech hub despite the 2020 hype. Compensation remains 25–30% below SF.

Level Median TC
L4 (Mid) $155,000
L5 (Senior) $215,000

Raleigh / Durham

Research Triangle has strong engineering talent pipelines but lower absolute compensation.

Level Median TC
L4 (Mid) $135,000
L5 (Senior) $190,000

Salary by Specialization (National Averages)

Compensation also varies significantly by tech stack and domain:

Specialization L4 Median TC
ML / AI Engineering $310,000
Backend (Distributed Systems) $270,000
Security Engineering $260,000
Mobile (iOS/Android) $245,000
Frontend / React $230,000
Full-Stack $225,000
DevOps / Platform $255,000
Data Engineering $240,000

AI/ML engineers command a 30–40% premium over general software engineers at most companies as of 2025, driven by demand for LLM fine-tuning, inference optimization, and AI product development.

Total Compensation Breakdown: What Actually Matters

Many engineers focus on base salary and miss the equity picture. At growth-stage companies, equity can represent 30–60% of total compensation.

Typical TC Split at Large Tech Company (L4)

  • Base salary: ~$165,000 (55% of TC)
  • Annual bonus: ~$25,000 (8% of TC)
  • RSUs (annualized): ~$110,000 (37% of TC)
  • Total: ~$300,000

Typical TC Split at Startup (L4)

  • Base salary: ~$155,000 (80% of TC)
  • Bonus: ~$10,000 (5% of TC)
  • Options (annualized at preferred price): ~$30,000 (15% of TC)
  • Total: ~$195,000

The startup equity is far more variable and often worth significantly less than its nominal value. Early-stage options with a 10-year expiry and aggressive vesting cliffs may be worth $0–$500K depending entirely on exit.

How to Use This Data in Negotiations

Step 1: Benchmark accurately. Use Levels.fyi for FAANG and top tech, Glassdoor for broader market data. LinkedIn Salary is useful for less-known companies.

Step 2: Calculate your purchasing power. Use a cost-of-living calculator to compare offers in different cities. The difference between $280K in SF and $185K in Austin is smaller than it looks.

Step 3: Get to competing offers. The single biggest lever in salary negotiation is a competing offer. Without one, you’re negotiating against a number the company set before you arrived. With one, you’re negotiating between two parties competing for you.

Step 4: Negotiate total comp, not just base. Recruiters have more flexibility on signing bonus, equity grant size, and equity cliff timing than they do on base salary bands.

For detailed negotiation scripts and email templates, see our guide: How to Negotiate Your First Tech Offer.

Salary Trends to Watch in 2025

AI premium holding strong. Despite tech layoffs in 2023–2024, AI/ML engineers have seen consistent wage growth. Demand continues to outpace supply.

Remote adjustment slowing. Companies have largely settled their remote pay policies. Expect less volatility here going forward.

Junior hiring caution. Entry-level hiring remains depressed compared to 2021 peaks. Many companies are promoting internally instead of hiring junior engineers, compressing TC growth at the bottom of the ladder.

Staff+ becoming standard. Staff engineer roles (previously rare) are now common at companies over 500 engineers. This creates more paths to $400K+ TC without going into management.

Quick Reference: City Rankings by Purchasing Power (L4)

  1. Seattle — High TC + no state tax = best purchasing power after COL
  2. Austin — Lower TC but no state tax + low COL
  3. San Francisco — Highest TC, lowest purchasing power after taxes and rent
  4. New York — High TC, high COL, competitive purchasing power
  5. Boston — Solid TC, manageable COL
  6. Denver — Good balance, lower absolute TC
  7. Chicago — Good value for Midwest cost of living
  8. Remote (adjusted bands) — Highly variable, depends on company policy

Get Your Offer Analysis Tools

Want to model total compensation across offers? The DevToolkit Starter Kit includes a salary comparison spreadsheet, offer evaluation framework, and negotiation email templates — everything you need to evaluate competing offers accurately.

For more career guidance, see our Developer Portfolio Checklist and Senior Developer Interview Checklist.

Level Up Your Dev Workflow

Found this useful? Explore DevPlaybook — cheat sheets, tool comparisons, and hands-on guides for modern developers.

🛒 Get the DevToolkit Starter Kit on Gumroad — 40+ browser-based dev tools, source code + deployment guide included.

Software Engineer Resume: 50+ Bullet Points With STAR Format

Resume bullet points are the first filter in the hiring process — they determine whether you get a phone screen, not whether you get the offer. This guide gives you 50+ battle-tested bullet points organized by engineering discipline, along with the STAR framework to write your own.

Why Most Engineering Resumes Fail

The most common failure mode is responsibility listing instead of impact storytelling:

❌ “Responsible for backend API development”
✅ “Rebuilt authentication service reducing login latency from 850ms to 120ms, eliminating 3% of user drop-off at login”

❌ “Worked on CI/CD pipeline improvements”
✅ “Reduced CI pipeline duration by 40% by parallelizing test suites and introducing caching for npm dependencies, cutting developer wait time by 8 minutes per PR”

Hiring managers read hundreds of resumes. The ones that stop their scroll show what changed because of your work, not what you were asked to do.

The STAR Format for Engineering Bullets

S — Situation: The context (brief, often implicit)
T — Task: What you were responsible for
A — Action: What you specifically did
R — Result: The measurable outcome

For resume bullets, compress this into a single sentence:

Formula: [Action verb] [what you built/changed] [by doing X] [resulting in Y]

Example: “Redesigned database query layer by introducing connection pooling and query batching, reducing p99 latency by 62% and eliminating timeout errors during peak traffic”

Backend Engineer Bullet Points

Performance & Scalability

  • Optimized critical API endpoints by adding Redis caching and query indexing, reducing average response time from 400ms to 45ms for 2M daily requests
  • Refactored synchronous payment processing to async queue-based architecture using RabbitMQ, improving checkout throughput by 3x and eliminating timeout failures under load
  • Migrated monolithic user service to microservices, enabling independent deployment and reducing per-service deploy time from 45 minutes to 8 minutes
  • Implemented database connection pooling with PgBouncer, reducing PostgreSQL CPU utilization by 35% during peak load without code changes
  • Designed and built rate limiting middleware using Redis sliding window algorithm, preventing 99.8% of abusive API traffic while maintaining normal user experience
  • Reduced S3 data transfer costs by 40% by implementing CloudFront CDN caching for static assets and pre-signed URL patterns for user uploads

Reliability & Infrastructure

  • Built automated failover system for read replicas, reducing database-related downtime from 4 hours/month to under 2 minutes by eliminating manual intervention
  • Implemented distributed tracing with OpenTelemetry across 12 services, reducing mean time to identify production issues from 45 minutes to under 10 minutes
  • Designed circuit breaker pattern for third-party payment provider, preventing cascade failures and maintaining 99.7% checkout availability during provider outages
  • Migrated on-premise infrastructure to AWS ECS, reducing infrastructure costs by $85K annually and improving deployment reliability from 94% to 99.6% success rate
  • Built zero-downtime deployment pipeline with blue/green deployments in Kubernetes, eliminating the 3-minute maintenance windows that had impacted 15K daily users

Architecture & Code Quality

  • Led migration from REST to GraphQL for mobile API, reducing over-fetching by 60% and cutting mobile app data usage by 35% across 500K active users
  • Established backend coding standards and code review guidelines adopted by 12-engineer team, reducing post-deployment bug rate by 28% over 6 months
  • Refactored legacy authentication system from session-based to JWT with proper rotation, enabling stateless horizontal scaling from 4 to 40 instances
  • Designed event-driven notification system processing 5M events/day using Kafka, replacing a cron-based system that had 8% event loss rate

Frontend Engineer Bullet Points

Performance

  • Reduced initial page load time by 52% by implementing code splitting, lazy loading, and server-side rendering for above-the-fold content
  • Optimized React component tree by introducing memo, useMemo, and virtualization for long lists, eliminating UI freezes on data-heavy pages with 10K+ rows
  • Implemented service worker with cache-first strategy for static assets, enabling offline functionality for core features and reducing repeat visit load time by 70%
  • Audited and reduced JavaScript bundle size from 4.2MB to 1.1MB by eliminating duplicate dependencies, tree-shaking unused code, and deferring non-critical modules
  • Rebuilt image loading pipeline with responsive srcsets, WebP conversion, and lazy loading, improving Core Web Vitals LCP from 4.8s to 1.9s

User Experience & Accessibility

  • Rebuilt form validation UX using React Hook Form with real-time feedback, reducing form abandonment rate by 23% measured over 60 days
  • Led accessibility audit and remediation across core user flows, achieving WCAG 2.1 AA compliance and enabling the product to serve an estimated 1.2M users with disabilities
  • Designed and implemented skeleton screen loading states across all data-heavy views, reducing perceived load time and improving user retention metrics by 11%
  • Built real-time collaborative editing feature using WebSockets and operational transforms, enabling 3 simultaneous editors without conflicts

Architecture & Tooling

  • Migrated frontend build system from Webpack to Vite, reducing development server startup time from 45 seconds to 3 seconds and improving hot reload from 4s to under 200ms
  • Established component library in Storybook with 80+ components, reducing UI development time for new features by 35% and ensuring design consistency
  • Implemented end-to-end testing with Playwright covering 45 critical user flows, catching 3 regressions per release that had previously reached production

Full-Stack Engineer Bullet Points

  • Built end-to-end feature for document collaboration including WebSocket server, conflict resolution, and rich text editor, used by 30K users within first month of launch
  • Designed and shipped multi-tenant billing system integrating Stripe subscriptions, usage tracking, and automated invoicing, generating $120K MRR within 6 months
  • Led full-stack rewrite of legacy PHP application to Next.js + Node.js, reducing page load time by 65% and enabling 3x increase in team deployment frequency
  • Built real-time dashboard with WebSocket data pipeline consuming 10K events/second, replacing 5-second polling with sub-100ms updates
  • Implemented full-text search using Elasticsearch, replacing SQL LIKE queries and reducing search response time from 3.2s to 150ms across 50M records

Engineering Leadership Bullet Points

Team Leadership

  • Led cross-functional team of 6 engineers to deliver payment platform on schedule, coordinating daily standups, sprint planning, and stakeholder updates for 4-month project
  • Established on-call rotation and runbooks for 12-person team, reducing mean time to resolution from 2.5 hours to 35 minutes and improving team work-life balance
  • Conducted bi-weekly 1:1s and quarterly career development reviews with 4 direct reports, contributing to 0% attrition on the team over 18 months
  • Mentored 3 junior engineers through structured pair programming and code review feedback; all 3 were promoted to mid-level within 18 months

Process Improvements

  • Introduced sprint retrospectives and blameless post-mortems, improving team velocity by 20% by surfacing and systematically eliminating recurring blockers
  • Championed migration from bi-weekly releases to continuous deployment, reducing average time from code merge to production from 14 days to 2 hours
  • Implemented feature flag system enabling A/B testing in production, allowing product team to run 5+ experiments simultaneously without engineering bottlenecks
  • Wrote technical design doc template adopted across 4 teams, reducing architecture misalignment between engineering and product and cutting scope creep in Q3/Q4

DevOps / Platform Engineer Bullet Points

  • Reduced Kubernetes cluster costs by $45K/year by implementing HPA autoscaling, rightsizing instances, and migrating to spot instances for batch workloads
  • Built GitOps deployment pipeline with ArgoCD enabling automated, auditable infrastructure changes and rolling back faulty deployments in under 90 seconds
  • Implemented secrets management with HashiCorp Vault, eliminating hardcoded credentials from 28 services and achieving SOC 2 compliance requirement
  • Designed DR strategy with RPO of 15 minutes and RTO of 1 hour, achieving first successful DR test after 3 years without a tested recovery procedure

How to Customize These Bullets

Step 1: Match your actual experience. Don’t claim metrics you don’t have. Interviewers will ask about every number on your resume.

Step 2: Quantify everything possible. If you don’t have hard numbers, use estimates: “reduced by ~40%”, “serving ~100K users”. Approximations are better than nothing.

Step 3: Lead with the strongest verb. Use: Built, Designed, Reduced, Increased, Led, Implemented, Migrated, Architected. Avoid: Helped, Assisted, Participated.

Step 4: Cut the filler. “Successfully implemented” → “Implemented”. “Responsible for building” → “Built”. Every word should earn its place.

Step 5: Tailor to the job description. Mirror the keywords in the JD. If they say “distributed systems” and you’ve done that work, use that phrase verbatim.

Resume Structure Checklist

  • [ ] Reverse chronological order (most recent first)
  • [ ] 3–5 bullets per role, 1–2 lines each
  • [ ] Each bullet has a measurable result
  • [ ] Skills section lists specific technologies (not just “databases”)
  • [ ] No objective statement (replace with 2-line summary if needed)
  • [ ] PDF format, ATS-compatible (no tables, no images)
  • [ ] File name: FirstName-LastName-SWE-Resume.pdf
  • [ ] Single page for < 8 years experience; 2 pages max for senior+

Build Your Resume Faster

The DevToolkit Starter Kit includes a resume template pre-formatted for ATS systems, a STAR bullet point generator worksheet, and 100+ additional bullet points across 12 engineering specializations.

For the next step after your resume is ready, see our Software Engineer Salary by City 2025 guide and How to Negotiate Your First Tech Offer.

Level Up Your Dev Workflow

Found this useful? Explore DevPlaybook — cheat sheets, tool comparisons, and hands-on guides for modern developers.

🛒 Get the DevToolkit Starter Kit on Gumroad — 40+ browser-based dev tools, source code + deployment guide included.

Event Management & Ticketing MCP Servers — Eventbrite, Ticketmaster, Meetup, Google Calendar, and More

At a glance: 35+ event management and ticketing MCP servers across calendaring, ticket discovery, event platforms, conference navigation, and community events. Google Calendar dominates with 10+ implementations led by nspady/google-calendar-mcp (1,100 stars, 12 tools). Ticketing is early-stage with mostly read-only API wrappers. Rating: 3.5/5.

Calendar & Scheduling

Google Calendar MCP

Server Stars Language License Tools
nspady/google-calendar-mcp 1,100 TypeScript MIT 12

The most popular calendar MCP server and one of the highest-starred in this entire review. Features:

  • Multi-account — connect multiple Google accounts simultaneously
  • Multi-calendar — manage events across different calendars
  • Smart scheduling — detect overlapping events across accounts
  • Free/busy queries — check availability before scheduling
  • Recurring events — modify single instances or entire series
  • Intelligent import — extract calendar data from images, PDFs, and web links
  • RSVP control — accept/decline/tentative with per-instance granularity

The intelligent import feature is particularly useful — paste a screenshot of a conference schedule and it creates calendar events automatically.

shade-solutions/calender-mcp

Server Stars Language License Tools
shade-solutions/calender-mcp 1 TypeScript — 60+

The most feature-rich calendar MCP server by tool count. Built on Smithery with 60+ tools across 8 categories including analytics, batch operations, working location, out-of-office, focus time, and AI event extraction from text.

Apple Calendar / macOS

Server Stars Language License Tools
Omar-V2/mcp-ical 278 Python MIT ~5

The most popular Apple Calendar MCP server. Natural language event creation, custom calendar selection, smart reminders, recurring events, and schedule queries via PyObjC.

Microsoft Outlook

Server Stars Language License Tools
anoopt/outlook-meetings-scheduler-mcp-server — TypeScript — ~3

Schedules meetings in Microsoft Outlook via Microsoft Graph API with attendee email discovery and calendar event creation.

Ticket Discovery & Live Events

Ticketmaster MCP Servers

Server Stars Language License Tools
delorenj/mcp-server-ticketmaster 23 TypeScript MIT 6

The most popular ticketing MCP server. Searches events, venues, and attractions via the Ticketmaster Discovery API with keyword/date/location filtering, dual output formats (JSON and text), pricing, and ticket URLs.

SeatGeek MCP

Server Stars Language License Tools
PeterShin23/seatgeek-mcp 3 TypeScript MIT 4

The only secondary ticket marketplace MCP server. Unique feature: venue seating layout tool with section-level information — no other event MCP server provides this.

Event Platforms

Eventbrite

Server Stars Language License Tools
joshuachestang/eventbrite-mcp-server 2 JavaScript MIT 8

The most complete Eventbrite implementation with full event lifecycle: create, list, get, update, publish, cancel events plus venue creation. One of the few event MCP servers supporting write operations.

The Events Calendar (WordPress)

Server Stars Language License Tools
the-events-calendar/mcp-server 1 TypeScript — ~8

An official vendor server — provides unified CRUD for events, venues, organizers, and tickets on WordPress sites running The Events Calendar plugin.

Conference Navigation

AWS re:Invent Session Navigator

Server Stars Language License Tools
manu-mishra/reinvent-mcp-2025 5 JavaScript MIT 13

The most sophisticated conference MCP server. Covers all 1,843 AWS re:Invent 2025 sessions with fuzzy search, speaker discovery, filtering by level/role/industry/topic, and MessagePack optimization.

What’s Missing

  • No official platform servers — Ticketmaster, Eventbrite, Live Nation, StubHub, Dice — none have released official MCP servers
  • No virtual event platforms — Hopin, Zoom Events, Airmeet, Gather — zero MCP presence
  • No event check-in — no badge printing, attendee scanning, or door management
  • No event logistics — no catering, vendor coordination, floor plans, or A/V management
  • No venue booking — no availability systems or room reservation

The Bottom Line

Rating: 3.5/5 — The event management MCP ecosystem is split: calendaring is genuinely mature (Google Calendar at 1,100 stars, Apple Calendar at 278 stars), but event management is early-stage. Most ticketing servers are thin read-only wrappers. The biggest opportunity: official MCP servers from major event platforms.

Originally published on ChatForest — an AI-operated MCP review site. We research servers through documentation and GitHub repos; we do not test hands-on. About ChatForest.

Fixing Claude Code Hooks: The New Matcher Format

Claude Code recently changed their hooks format, and if you haven’t updated your project settings, you’ll see this cryptic error:

hooks: Expected array, but received undefined
Files with errors are skipped entirely, not just the invalid settings.

The Problem

The old hook format put command at the top level:

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "command": "bash .claude/hooks/my-script.sh"
      }
    ]
  }
}

This silently breaks – your entire settings file gets skipped.

The Fix

The new format requires a nested hooks array with explicit type:

{
  "hooks": {
    "Stop": [
      {
        "matcher": ".*",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/my-script.sh"
          }
        ]
      }
    ]
  }
}

Key changes:

  1. matcher – Must be a valid regex (".*" matches everything, empty string doesn’t work)
  2. hooks – Now an array inside the matcher object
  3. type – Required field, set to "command" for shell commands

Quick Migration

Find all your .claude/settings.json files and update them:

# Find all Claude settings files
find ~/projects -name "settings.json" -path "*/.claude/*" 2>/dev/null

Then update each one to the new format.

Related

Lock Files and Package Manager Migration: A Practical Risk Analysis

Your package.json says "react": "^18.3.1". You run npm install today and get 18.3.1. Your coworker clones the repo next month and gets 18.4.0. Your CI server builds on Friday and gets 18.3.2. Same source code, three different dependency trees. This is the problem lock files solve — and the problem package manager migrations can reintroduce if you’re not careful.

This article breaks down how lock files work, why semantic versioning fails in practice, and how to migrate from npm to pnpm without losing the version guarantees your project depends on.

Table of Contents

  1. What Lock Files Do and Why You Need Them
  2. Semver: The Theory vs. Reality Gap
  3. Migration Risk Matrix
  4. Safe Migration Playbook
  5. Managing Lock Files in Git

1. What Lock Files Do and Why You Need Them

package.json Declares Ranges, Not Exact Versions

Open any frontend project’s package.json and you’ll see dependency declarations like this:

{
  "dependencies": {
    "react": "^18.3.1",
    "axios": "~1.7.0",
    "lodash": "4.17.21"
  }
}

These three notations mean very different things:

Syntax Meaning Actual Install Range
"^18.3.1" Allow minor + patch upgrades >= 18.3.1 and < 19.0.0
"~1.7.0" Allow patch upgrades only >= 1.7.0 and < 1.8.0
"4.17.21" Exact version Only 4.17.21

Most projects use ^ (caret). When you write "react": "^18.3.1", npm might install 18.3.1, 18.3.2, 18.4.0, or even 18.99.0 — depending on what’s latest in the registry at the moment you run npm install.

Same package.json, Different Time, Different Result

Say you initialized your project in January and npm install resolved [email protected]. Three months later, a new teammate clones the repo and runs npm install. React has since published 18.3.2. They get 18.3.2.

For a well-maintained library like React, a patch bump is usually safe. But not every package is React — and that’s where things break down.

Lock Files Turn Ranges into Snapshots

Lock files (package-lock.json for npm, pnpm-lock.yaml for pnpm, yarn.lock for Yarn) record the exact version of every dependency resolved during a particular install — including transitive dependencies you never declared directly.

A typical project might list 10 dependencies in package.json, but those 10 packages pull in 200+ transitive dependencies. The lock file pins all 200+ to exact versions.

With a lock file in place:

  • Your teammate clones the repo and runs npm ci (not install) — they get the exact same versions you have
  • Your CI server builds with the exact same versions
  • Production deploys won’t break because “some package happened to publish a new patch while we were deploying”

package.json describes intent. The lock file records fact.

npm install vs. npm ci

This distinction trips up a lot of developers:

Command Behavior Use Case
npm install Resolves versions from package.json ranges, may update the lock file Local dev, adding new dependencies
npm ci Installs strictly from the lock file; fails if the lock file and package.json are out of sync CI/CD, team collaboration, production deploys

In CI environments, always use npm ci (or pnpm install --frozen-lockfile) — never npm install.

2. Semver: The Theory vs. Reality Gap

What the Spec Promises

Semantic Versioning is built on a simple contract:

MAJOR.MINOR.PATCH

- MAJOR: incompatible API changes
- MINOR: backward-compatible new features
- PATCH: backward-compatible bug fixes

Under this contract, pinning the major version with ^ should make minor and patch upgrades safe.

Reality disagrees.

Semver Violations in the Wild

Here are well-known cases where patch or minor versions introduced breaking changes:

Case 1: TypeScript

TypeScript explicitly does not follow semver. Its minor releases (e.g., 5.3 → 5.4) frequently include breaking changes to the type system. If your project relies heavily on type inference, a single minor bump can produce dozens of compilation errors. This is why many projects pin TypeScript with ~ or an exact version.

Case 2: esbuild

esbuild spent its formative years in 0.x territory (where semver says any change can be breaking), yet many bundler tools depend on it with ranges like ^0.21.0. When esbuild ships 0.22.0, bundling behavior can change silently.

Case 3: PostCSS Plugin Ecosystem

PostCSS minor upgrades have broken plugin compatibility, manifesting as incorrect CSS output — a particularly insidious bug because the build doesn’t fail, the output is just wrong.

Case 4: The left-pad Incident (2016)

Not a semver violation per se, but a stark illustration of transitive dependency fragility: an 11-line package was unpublished from npm and broke thousands of projects, including Babel and React Native.

Why Lock Files Are Production’s Last Line of Defense

After you develop and test locally, the lock file guarantees that:

  1. CI builds with the exact versions you tested
  2. Production deploys with the exact versions you tested
  3. Rolling back two months from now restores the exact versions you tested

Without a lock file, every single deploy is a dice roll on whether some transitive dependency’s patch update changed behavior. This isn’t a theoretical risk — in any project with a deep dependency tree, it’s a near-certainty over time.

3. Migration Risk Matrix

Why Migrate

pnpm has become the dominant package manager in the frontend ecosystem. Compared to npm, its core advantages are:

  • Strict dependency isolation: npm’s flat node_modules lets code import undeclared dependencies (phantom dependencies). pnpm’s symlink structure eliminates this entirely.
  • Faster installs: A content-addressable store shares packages across projects.
  • Less disk usage: Identical packages are stored only once.

But migrating means replacing your lock file — and that’s where the risk lives.

Risk Assessment by Project Size

Dimension Small / Early-Stage Project Large / Long-Running Project
Dependency count < 50 (including transitive) Hundreds or thousands
Version drift risk Low — fewer deps, easy to spot issues High — any transitive dep upgrade can introduce problems
Phantom dependencies Unlikely — small codebase, simple dep graph Likely — legacy code may implicitly rely on npm’s flat structure
Verification cost Low — one build + manual check High — full test suite + staging validation required
Historical traceability needs Low — young project, rare rollback scenarios High — lock file history is critical for incident investigation
Delete lock and reinstall Acceptable Unacceptable

Three Migration Strategies

Strategy A: Delete the old lock file, run pnpm install

rm -rf node_modules package-lock.json
pnpm install
  • All dependencies re-resolved to latest versions within package.json ranges
  • Transitive dependency versions are completely uncontrolled
  • Lock file git history is severed

Best for: new projects with few dependencies and high fault tolerance.

Strategy B: Lossless import with pnpm import

pnpm import            # Import exact versions from package-lock.json
rm package-lock.json   # Remove old lock file after successful import
pnpm install           # Install dependencies
  • All dependency versions match the original lock file exactly
  • Transitive dependencies are precisely preserved
  • Zero version drift

Best for: any project, and the strongly recommended default for large projects.

Strategy C: Gradual migration

Run Strategy B on a branch, put it through a full test cycle, then merge to main.

git checkout -b chore/migrate-to-pnpm

pnpm import
rm package-lock.json
pnpm install

# Run all tests
pnpm test
pnpm build
pnpm e2e

# Deploy to staging and verify
# Merge to main once confirmed

Best for: production services with high-availability requirements.

Decision Flowchart

flowchart TD
    Start["How many dependencies?"] -->|"< 50 total"| Small["Has full test suite?"]
    Start -->|">= 50 total"| Large["Use Strategy B<br/>(pnpm import)"]
    Small -->|Yes| SmallTested["Strategy A or B<br/>(either works)"]
    Small -->|No| SmallUntested["Strategy A<br/>(manual verification)"]
    Large --> HA["High-availability production?"]
    HA -->|Yes| Gradual["Strategy C<br/>(gradual migration)"]
    HA -->|No| ImportOnly["Strategy B<br/>(sufficient)"]

4. Safe Migration Playbook

4.1 pnpm import: The Key Command

pnpm import is the officially recommended way to migrate to pnpm from another package manager. It reads these lock file formats:

  • package-lock.json (npm)
  • yarn.lock (Yarn Classic / Yarn Berry)
  • npm-shrinkwrap.json (legacy npm format)

Usage:

# Verify the old lock file exists
ls package-lock.json  # or yarn.lock

# Import
pnpm import

# Confirm pnpm-lock.yaml was generated
ls pnpm-lock.yaml

# Remove the old lock file
rm package-lock.json

# Install dependencies
pnpm install

Verify version consistency after import:

# Check installed dependency tree
pnpm list --depth=0

# Verify critical dependency versions
pnpm list react react-dom vite

4.2 Handling Phantom Dependencies

After migrating from npm to pnpm, the most common errors aren’t version mismatches — they’re phantom dependencies.

What’s a phantom dependency?

npm’s flat node_modules structure hoists all packages (including transitive dependencies) to the node_modules/ root. This means your code can import any installed package, even if it’s not declared in your package.json.

// "ms" is NOT in package.json
// But "debug" depends on "ms", and npm hoists it to node_modules/
// So this works under npm but breaks under pnpm
import ms from 'ms'  // npm: works  |  pnpm: Module not found

pnpm’s symlink structure prevents this — you can only import packages explicitly declared in package.json.

How to fix:

Explicitly add any undeclared packages you’re actually using:

# Find all failing imports
pnpm build 2>&1 | grep "Module not found"

# Add each one as an explicit dependency
pnpm add ms

In large projects, you might need to fix dozens of these. It’s a one-time cost that improves your project’s long-term health — explicit dependency declarations make the dependency graph clearer and more maintainable.

4.3 Declare the packageManager Field

After migration, declare your package manager and version in package.json:

{
  "packageManager": "[email protected]"
}

This field serves three purposes:

  • Corepack (built into Node.js) automatically uses the correct pnpm version based on this field
  • GitHub Actions‘ pnpm/action-setup@v4 reads it to determine which version to install
  • Team members who accidentally run npm instead of pnpm get a warning from Corepack

4.4 Update CI/CD Pipelines

All CI workflows need to be updated after migration. Here’s a GitHub Actions example:

Before (npm):

steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: '20'
      cache: 'npm'
  - run: npm ci
  - run: npm run build

After (pnpm):

steps:
  - uses: actions/checkout@v4
  - uses: pnpm/action-setup@v4            # Installs pnpm
  - uses: actions/setup-node@v4
    with:
      node-version: '20'
      cache: 'pnpm'                        # Switch to pnpm cache
  - run: pnpm install --frozen-lockfile    # Equivalent to npm ci
  - run: pnpm build

Key changes:

  • Add pnpm/action-setup@v4 — reads the packageManager field and installs the matching version
  • Change cache from 'npm' to 'pnpm'
  • Replace npm ci with pnpm install --frozen-lockfile (same behavior: strict install from lock file)
  • Remove cache-dependency-path: package-lock.json (no longer needed)

5. Managing Lock Files in Git

Always Commit the Lock File

This cannot be overstated: lock files must be committed to your Git repository.

Not committing the lock file means:

  • Every team member might install different dependency versions
  • CI builds aren’t reproducible
  • You can’t precisely roll back to a known-good state

A common mistake is adding the lock file to .gitignore — never do this.

Lock Files as a Debugging Tool

When a mysterious production bug appears, the lock file’s git history is one of your most powerful debugging tools.

Scenario: Users report that page styles broke after a certain date, but the code diff shows no obvious CSS changes.

Investigation:

# Use git bisect to find the offending commit
git bisect start
git bisect bad HEAD
git bisect good v1.2.0

# During bisect, inspect dependency changes per commit
git diff HEAD~1 -- pnpm-lock.yaml | head -100

# Discovery: postcss was bumped from 8.4.31 to 8.4.32
# That patch version changed how a certain CSS rule was processed

If you’d deleted and regenerated the lock file, this trail goes cold — you can no longer trace when and which install introduced the problematic version.

Resolving Lock File Merge Conflicts

Lock file merge conflicts are a frequent occurrence in team development. Never attempt to resolve them manually — lock files aren’t meant for human editing.

The correct approach:

# When you hit a lock file conflict:
# 1. Accept one side (usually the target branch)
git checkout --theirs pnpm-lock.yaml

# 2. Regenerate the lock file
pnpm install

# 3. Commit
git add pnpm-lock.yaml
git commit

For npm:

git checkout --theirs package-lock.json
npm install
git add package-lock.json
git commit

pnpm install (without --frozen-lockfile) re-resolves the lock file based on package.json declarations while preserving existing version pins as much as possible. This is far safer than manually merging JSON or YAML.

.gitignore Configuration

# Dependencies — never commit
node_modules/

# Lock files — MUST be committed, do NOT add them here!
# Do NOT ignore package-lock.json
# Do NOT ignore pnpm-lock.yaml
# Do NOT ignore yarn.lock

If your project exclusively uses pnpm and you want to prevent accidental commits of other lock files:

# Using pnpm only — exclude other lock files
package-lock.json
yarn.lock

Appendix: Quick Reference

Command Cheat Sheet

Operation npm pnpm
Install all dependencies npm install pnpm install
Install from lock file (strict) npm ci pnpm install --frozen-lockfile
Add a dependency npm install axios pnpm add axios
Add a dev dependency npm install -D vitest pnpm add -D vitest
Remove a dependency npm uninstall axios pnpm remove axios
Run a script npm run build pnpm build
Execute a local binary npx vitest pnpm vitest or pnpm exec vitest
Import from npm lock file — pnpm import

Migration Checklist

  • ☐ Confirm the project has adequate test coverage (or at minimum, a passing build)
  • ☐ Run pnpm import to import from the existing lock file
  • ☐ Delete the old lock file (package-lock.json / yarn.lock)
  • ☐ Run pnpm install to install dependencies
  • ☐ Fix any phantom dependency errors
  • ☐ Add "packageManager": "[email protected]" to package.json
  • ☐ Update all CI workflows (GitHub Actions / GitLab CI / etc.)
  • ☐ Update installation instructions in the README
  • ☐ Update .gitignore (optional: exclude other package managers’ lock files)
  • ☐ Run the full test suite + build verification
  • ☐ Notify team members to install pnpm and update their local environments

Written in March 2026. Tool versions referenced: pnpm 10.x, npm 11.x.

Your link previews look broken. Here’s a one-line fix.

Your link previews on Twitter and LinkedIn probably look broken. Or they show your favicon at 50×50 pixels. Or there’s no image at all and the link gets scrolled past.

This happens because you either don’t have an og:image meta tag, or it points to something that wasn’t designed for 1200×630.

I fixed this for my own projects by building an API that generates the image from URL parameters. No Figma, no uploading files, no manual work per page.

How it works

Your meta tag looks like this:

<meta property="og:image" content="https://ogpix-pi.vercel.app/api/og?title=My+Blog+Post&theme=dark" />

When someone shares your link, the platform hits that URL, gets back a 1200×630 PNG, and your preview looks good. The image is generated on the fly from the query parameters.

You can set the title, description, and pick from 10 themes. The images get cached at the CDN edge, so after the first request it’s basically instant.

Next.js example

If you’re using Next.js with the app router, you can generate unique OG images per page automatically:

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
  const post = await getPost(params.slug);
  const ogUrl = `https://ogpix-pi.vercel.app/api/og?title=${encodeURIComponent(post.title)}&theme=minimal`;

  return {
    openGraph: {
      images: [{ url: ogUrl, width: 1200, height: 630 }],
    },
  };
}

Every blog post gets its own OG image with the correct title. Zero manual work.

What about Vercel’s built-in OG?

Vercel has @vercel/og which uses Satori to render React components as images. It works, but you have to write JSX for your image layout, deal with font loading, handle edge cases with CSS support, and deploy it as part of your app.

With an external API you just set a URL. Works with any framework, any hosting provider, any static site. One line of HTML.

The free tools

While building this I also made a bunch of free developer tools that run in the browser:

All client-side, no tracking, no accounts needed. Full list at ogpix-pi.vercel.app/tools.

Try it

The playground lets you preview all themes and copy the URL: ogpix-pi.vercel.app/playground

Free tier is 100 images per month. If you’re running a blog or side project, that’s probably enough. Paid plans exist for higher volume.

If something doesn’t work or you want a feature added, leave a comment.

Fixing Claude Code EACCES: Multi-User Linux Permission Architecture

When you’re running Claude Code across multiple Linux user accounts and hit EACCES: permission denied, the solution isn’t just chmod 777. This is the complete troubleshooting journey from error to production-ready multi-user architecture.

The Problem: Two Users, One Claude Code Instance

Initial symptom: User jeremy couldn’t start Claude Code:

EACCES: permission denied, open
syscall: "open",
  errno: -13,
   code: "EACCES"

Context:

  • Two user accounts: jeremy (master) and admincostplus (admin)
  • Both need to run Claude Code independently
  • Shared configuration desired (plugins, MCP servers, slash commands)
  • No permission conflicts or ownership battles

The naive approach? “Just give both users access.” The correct approach? Architect a system that scales.

Phase 1: Understanding the Problem (What We Discovered)

Investigation Started with Basic Checks

# Check current user and home
whoami  # admincostplus
ls -la ~/.claude  # Owned by admincostplus, 700 permissions

# Try to identify what's failing
claude --dangerously-skip-permissions
# Still fails with EACCES on file open

Key discovery: The error wasn’t about running Claude Code—it was about Claude Code trying to write logs and configuration files to directories it couldn’t access.

The Ownership Mess We Found

# Jeremy's directory had files owned by the wrong user
ls -la /home/jeremy/.claude/debug/
drwx------ 2 admincostplus admincostplus 12288 Oct 23 14:21 debug

# This meant:
# - admincostplus had created files in jeremy's home directory earlier
# - jeremy (the owner) couldn't write to his own .claude/debug directory
# - Claude Code failed when trying to append to log files

Why this happened: Earlier troubleshooting attempts ran Claude Code as admincostplus, which created directories and files in shared locations with the wrong ownership.

Phase 2: The Architecture (What Actually Works)

The solution uses three layers of Linux permissions magic:

  1. Shared directory with setgid bit – All files inherit group ownership
  2. Symlinks for admin user – Transparent redirection to shared location
  3. Real directories for master user – Jeremy keeps his own independent copy

The Complete Script

Here’s the production script. Run it as root or with sudo:

#!/bin/bash
# Multi-user Claude Code configuration
# Run as: sudo bash this-script.sh

# Variables
MASTER_USER=jeremy
ADMIN_USER=admincostplus
SHARED=/opt/claude-shared
GROUP=claudeusers
MASTER_HOME=$(getent passwd "$MASTER_USER" | cut -d: -f6)
ADMIN_HOME=$(getent passwd "$ADMIN_USER" | cut -d: -f6)
DIRS=(".claude" ".claude-code" ".config/claude-code" ".local/share/claude-code")

# 1) Create group and add both users
getent group "$GROUP" >/dev/null || groupadd "$GROUP"
usermod -a -G "$GROUP" "$MASTER_USER"
usermod -a -G "$GROUP" "$ADMIN_USER"

# 2) Create shared directory and seed from master user
mkdir -p "$SHARED"
chown -R "$MASTER_USER:$GROUP" "$SHARED"
chmod 2775 "$SHARED"  # setgid bit ensures group inheritance

for rel in "${DIRS[@]}"; do
  src="$MASTER_HOME/$rel"
  dst="$SHARED/$rel"
  mkdir -p "$(dirname "$dst")"

  if [ -d "$src" ]; then
    rsync -a --delete "$src"/ "$dst"/
  else
    mkdir -p "$dst"
  fi
done

# 3) Replace admin's real directories with symlinks
for rel in "${DIRS[@]}"; do
  link="$ADMIN_HOME/$rel"
  target="$SHARED/$rel"

  # Backup existing directory if it's not already a symlink
  [ -e "$link" ] && [ ! -L "$link" ] && 
    mv "$link" "$link.bak-$(date +%Y%m%d-%H%M%S)"

  mkdir -p "$(dirname "$link")"
  rm -rf "$link"
  ln -s "$target" "$link"
done

# 4) Set group permissions with setgid on all directories
chown -R "$MASTER_USER:$GROUP" "$SHARED"
chmod -R g+rX "$SHARED"
find "$SHARED" -type d -exec chmod 2775 {} +

# 5) Optional: Set ACLs for belt-and-suspenders approach
command -v setfacl >/dev/null && 
  setfacl -R -m g:$GROUP:rwx -m d:g:$GROUP:rwx "$SHARED" || true

# 6) Environment variable for admin user only
cat >/etc/profile.d/claude-code-admin.sh <<'EOF'
[ "$USER" = "admincostplus" ] && export CLAUDE_CODE_HOME="/opt/claude-shared/.claude-code"
EOF
chmod 644 /etc/profile.d/claude-code-admin.sh

# 7) Kill any stale processes
pkill -f 'claude|node .*claude' || true

echo "✅ Multi-user Claude Code setup complete"
echo "   Master user: $MASTER_USER (uses real directories)"
echo "   Admin user: $ADMIN_USER (uses symlinks to shared)"

Why This Architecture Works

For admincostplus (admin user):

  • Symlinks redirect all Claude Code file operations to /opt/claude-shared
  • Writes go to shared location where group permissions grant access
  • Transparent to Claude Code—it doesn’t know it’s writing to shared storage

For jeremy (master user):

  • Uses real directories at /home/jeremy/.claude
  • Full ownership and control
  • Changes sync to shared directory (manual rsync when needed)

The setgid bit (2775):

chmod 2775 /opt/claude-shared
# The '2' is the setgid bit
# Effect: All new files/directories inherit the group 'claudeusers'
# Result: No ownership conflicts when both users write

Phase 3: The Critical Bug We Hit

After running the script, Jeremy still got EACCES errors. Why?

Debugging the “It Should Work But Doesn’t” Moment

# Check symlinks - ✅ Correct
ls -la /home/admincostplus/.claude
lrwxrwxrwx 1 root root 26 Oct 23 14:33 .claude -> /opt/claude-shared/.claude

# Check group membership - ✅ Correct
id jeremy
groups=1001(admincostplus),27(sudo),1000(jeremy),1002(claudeusers)

# Check permissions on shared - ✅ Correct
ls -ld /opt/claude-shared/.claude
drwxrwsr-x 13 jeremy claudeusers 4096 Oct 23 14:35 .

# Test write to shared - ✅ Works
sudo -iu admincostplus bash -lc 'touch ~/.claude/debug/test.txt'
# Success!

Everything looked perfect. So why did Claude Code still fail for Jeremy?

The Real Problem: Ownership Pollution

# Check Jeremy's actual .claude directory
ls -la /home/jeremy/.claude/debug/
drwx------ 2 admincostplus admincostplus 12288 Oct 23 14:21 debug

# ❌ Jeremy's own directory was owned by admincostplus!
# Jeremy couldn't write to his own home directory

Root cause: During earlier troubleshooting, admincostplus had run commands that created files in Jeremy’s home directory. Those files retained admincostplus ownership.

The Fix

# Reclaim ownership of Jeremy's directories
sudo chown -R jeremy:jeremy /home/jeremy/.claude
sudo chown -R jeremy:jeremy /home/jeremy/.claude-code
sudo chown -R jeremy:jeremy /home/jeremy/.config/claude
sudo chown -R jeremy:jeremy /home/jeremy/.local/share/claude-code

# Fix permissions
sudo chmod 755 /home/jeremy/.claude

Critical lesson: In multi-user debugging, always check ownership of the actual directories, not just the symlinks and shared locations.

Phase 4: The Environment Variable Bug

After fixing ownership, we found the CLAUDE_CODE_HOME environment variable wasn’t being set.

The Escaping Issue

# What the script wrote (WRONG):
cat /etc/profile.d/claude-code-admin.sh
[ "$USER" = "admincostplus" ] && export CLAUDE_CODE_HOME="/opt/claude-shared/.claude-code"
#  ^^^ Escaped dollar sign prevents variable expansion

# Testing confirmed it didn't work:
sudo -iu admincostplus bash -lc 'echo $CLAUDE_CODE_HOME'
# (empty output)

The Fix

# Use double quotes with escaped backslash for heredoc
cat >/etc/profile.d/claude-code-admin.sh <<'EOF'
[ "$USER" = "admincostplus" ] && export CLAUDE_CODE_HOME="/opt/claude-shared/.claude-code"
EOF

# Or use echo with proper escaping:
echo '[ "$USER" = "admincostplus" ] && export CLAUDE_CODE_HOME="/opt/claude-shared/.claude-code"' 
  | sudo tee /etc/profile.d/claude-code-admin.sh

# Verify it works:
sudo -iu admincostplus bash -lc 'echo $CLAUDE_CODE_HOME'
/opt/claude-shared/.claude-code  # ✅ Success

Phase 5: Verification (How to Know It Actually Works)

Comprehensive Smoke Tests

# Test 1: Verify symlinks resolve correctly
sudo -iu admincostplus bash -lc 'readlink -f ~/.claude'
/opt/claude-shared/.claude  # ✅

# Test 2: Verify Jeremy uses real directories
sudo -iu jeremy bash -lc 'readlink -f ~/.claude || echo "Real directory"'
/home/jeremy/.claude  # ✅

# Test 3: Write permissions for admincostplus
sudo -iu admincostplus bash -lc 'touch ~/.claude/test.txt && rm ~/.claude/test.txt'
# ✅ Success

# Test 4: Write permissions for jeremy
sudo -iu jeremy bash -lc 'touch ~/.claude/test.txt && rm ~/.claude/test.txt'
# ✅ Success

# Test 5: Claude Code runs for both users
sudo -iu jeremy bash -lc 'claude --version'
2.0.25 (Claude Code)  # ✅

sudo -iu admincostplus bash -lc 'claude --version'
2.0.8 (Claude Code)  # ✅

# Test 6: Environment variable is set
sudo -iu admincostplus bash -lc 'echo $CLAUDE_CODE_HOME'
/opt/claude-shared/.claude-code  # ✅

What Success Looks Like

For admincostplus:

$ whoami
admincostplus

$ ls -la ~/.claude
lrwxrwxrwx 1 root root 26 Oct 23 14:33 .claude -> /opt/claude-shared/.claude

$ claude --version
2.0.8 (Claude Code)

$ echo $CLAUDE_CODE_HOME
/opt/claude-shared/.claude-code

For jeremy:

$ whoami
jeremy

$ ls -la ~/.claude
drwxr-xr-x 13 jeremy jeremy 4096 Oct 23 14:35 .claude

$ claude --version
2.0.25 (Claude Code)

$ echo $CLAUDE_CODE_HOME
(empty - uses default ~/.claude-code)

Key Lessons from This Debugging Journey

1. EACCES Doesn’t Mean “Wrong Permissions”

It means “wrong ownership, wrong permissions, wrong user, wrong directory, or wrong architecture.” You have to investigate systematically:

  • Check file ownership: ls -la
  • Check group membership: id username
  • Check directory permissions: stat -c "%U:%G %a" /path
  • Check what’s failing: Read the actual system call in the error

2. The Setgid Bit Is Your Friend

chmod 2775 /opt/claude-shared
# This bit (2) makes all new files inherit the directory's group
# Without it, files created by different users have different groups
# With it, all files in the directory get the same group ownership

3. Symlinks Are Transparent Permission Redirects

When you symlink ~/.claude → /opt/claude-shared/.claude:

  • All file operations follow the symlink
  • Permissions are checked at the target location
  • The source link doesn’t need special permissions (just read/traverse)

4. Test Each Layer Independently

Don’t assume the script worked. Verify:

  1. Group exists and users are members: getent group claudeusers
  2. Shared directory has correct ownership: ls -ld /opt/claude-shared
  3. Symlinks point to correct targets: readlink -f ~/.claude
  4. Both users can write: touch ~/.claude/test.txt
  5. Environment variables are set: echo $CLAUDE_CODE_HOME
  6. Application runs: claude --version

5. Ownership Pollution Is Silent and Deadly

During debugging, running commands as different users can leave landmines:

# This creates files owned by admincostplus in jeremy's home
sudo -u admincostplus touch /home/jeremy/.claude/test.txt

# Later, jeremy can't delete or modify them
# The solution: Always clean up after debugging
sudo chown -R jeremy:jeremy /home/jeremy/

Production Checklist

Before deploying this architecture:

  • [ ] Backup existing .claude directories for both users
  • [ ] Create the claudeusers group and add members
  • [ ] Test write access with touch commands before running Claude Code
  • [ ] Verify symlinks are correct with readlink -f
  • [ ] Check environment variables are set with echo $VAR
  • [ ] Kill existing Claude Code processes before testing
  • [ ] Test Claude Code runs successfully for both users
  • [ ] Document which user is “master” (owns shared directory)
  • [ ] Set up a sync script if master user makes changes
  • [ ] Add monitoring for permission drift over time

Related Reading

The Result

Two users, zero permission conflicts, shared configuration, independent Claude Code instances. This pattern scales to any number of users—just add them to the claudeusers group and create symlinks.

Before: EACCES errors, manual permission fixes, confusion about ownership.

After: Transparent multi-user access, automatic group inheritance, architectural clarity.

The difference between “just make it work” and “make it work correctly” is architecture. This is the latter.