Supercharge Your Workflow: Exploring Claude Code’s GitHub Integration & Hooks

If you’ve been using Claude Code in your terminal, you already know how powerful it is for local development. But did you know you can extend Claude Code’s capabilities directly into your GitHub repository and customize its behavior with lifecycle hooks?

In this post, we’ll walk through how to set up Claude Code to automatically review pull requests, fix issues in GitHub, and execute custom code using hooks.

1. Setting up the GitHub Integration

To use Claude Code outside of the terminal, we need to install its built-in GitHub integration, which is powered by its underlying software development kit (SDK).

To do this, simply run the following command in your terminal: /install-GitHub-app

This command will prompt you to authenticate via your command line interface and will open your browser to authorize the app. You will be asked to specify which workflows you want to install—such as tagging Claude in issues and enabling automated PR reviews—and to create a long-lived token.

2. Automated Code Reviews on Pull Requests

Once the setup is complete, Claude will automatically generate a pull request containing YAML configuration files for GitHub Actions. Out of the box, these default configurations enable bug fixes, testing, and code reviews.

Once you merge this initial setup PR, Claude becomes your newest automated teammate. For all future pull requests, a Claude GitHub Action will trigger automatically to:

  • Read and analyze files
  • Check code quality
  • Identify security considerations

3. Resolving GitHub Issues Autonomously

Claude doesn’t just review code; it can also write it directly from GitHub issues.

Imagine someone on your team creates an issue asking to revert a recently added UI header, specifically requesting to “remove the Course Materials Assistant header” and the horizontal row, while keeping the theme toggle intact.

Instead of assigning a human developer, you can simply tag Claude in the issue comments (e.g., “Claude, can you fix this for me?”). Claude will then:

  1. Spin up a job to analyze the repository structure.
  2. Create a plan to modify the codebase.
  3. Test the changes, commit them, and automatically generate a new Pull Request with the fix.

Fascinatingly, because we set up automated PR reviews earlier, Claude will even double-check and review its own code before you merge it in!

4. Customizing Operations with Hooks

Another incredibly powerful feature recently added to Claude Code is Hooks. Hooks allow you to inject and execute custom scripts at any point in the lifecycle of Claude Code’s operations (e.g., before a tool is executed, after a notification is sent, or when a subagent concludes).

To configure this, type /hooks in your terminal to open the configuration manager.

As a fun example, you can create a PostToolUse hook. You can define a “matcher” so that anytime Claude uses the Read or Grep tools, it triggers your computer’s audio to literally speak the words “All done!” using the terminal say command.

These configurations are saved safely in your project’s .claude/settings.local.json file. While making your computer talk is a fun trick, hooks can be used for serious automation, like strictly running tests or linters after every file modification, or preventing certain tools from running altogether.

What’s Next?

By integrating Claude Code with GitHub and utilizing local hooks, you can drastically reduce manual review times and automate repetitive tasks. Up next, you can even explore using Claude Code in slightly different environments, like refactoring Jupyter notebooks and creating data dashboards

Supercharge Your AI Development: How to Use Git Worktrees with Claude Code for Parallel Work

If you are using Claude Code as your AI programming assistant, you might run into a common issue: trying to build multiple features at once. Opening multiple instances of Claude in the same terminal environment can lead to overwriting files, bug creation, and a lot of confusion.

The solution? Git Worktrees.

Git worktrees allow you to essentially create copies of your codebase, operate on them in complete isolation, and merge them all together perfectly at the end. The best part is that Claude can manage almost all of this for you. Here is how you can easily use Git worktrees with Claude Code in your next project.

Step 1: Set Up Your Worktrees

To get started, you’ll want to create dedicated folders for your isolated codebases. Create a folder (for example, .trees) and use the git worktree add command to create separate environments for each feature you want to build.

For example, you could set up three parallel environments:

  • ui_feature for front-end design
  • testing_feature for backend endpoint tests
  • quality_feature for linting and code formatting tools.

Step 2: Run Claude Code in Parallel Environments

Next, open up a separate terminal window for each of your newly created worktree environments. You can now launch Claude Code in all of these terminals at the same time. Because each instance is isolated, you can confidently prompt Claude to work on different files and features simultaneously without worrying about overwriting the same file.

(Pro-tip: If you find yourself repeatedly giving Claude the same instructions—like standardizing how you want front-end changes written—you can create Custom Commands by placing markdown files in your project’s .claude/commands folder.)

Step 3: Commit Your Work

Once Claude has successfully implemented your features in each separate terminal, it’s time to save your work. Instead of manually crafting Git commit messages, you can simply ask Claude to do the heavy lifting. Just type: “Add and commit with a descriptive message”. Claude will read the changes it made and automatically generate a high-quality commit message for that specific worktree.

Step 4: Let Claude Handle the Merging and Conflicts

Now for the magic. Close your worktree terminals and hop back into your main branch terminal.

Since you worked on multiple features at once (like updating your pyproject.toml file in two different trees), there will likely be merge conflicts. Instead of manually resolving them, you can prompt Claude Code with: “Use the Git merge command to merge in all of the worktrees in the .trees folder and fix any conflicts if there are any.”

Claude Code knows exactly what to do. It will systematically merge each branch. If it detects conflicts in overlapping files, it will analyze the conflicts, resolve them intelligently, and complete the merge automatically.

Conclusion

By combining Claude Code’s powerful agentic abilities with Git worktrees, you can act as a true project manager—deploying multiple AI agents across all parts of your tech stack in parallel. You avoid the headaches of file overwriting, save time, and dramatically speed up your development workflow!

The Jest Functions You’ll Actually Use

A practical reference to the Jest functions that come up in almost every test suite — with a short example for each. New to Jest? Read top to bottom. Just looking something up? Jump to the section you need.

All examples assume a simple module under test:

// math.js
function add(a, b) { return a + b; }
function divide(a, b) {
  if (b === 0) throw new Error("Cannot divide by zero");
  return a / b;
}
module.exports = { add, divide };

1. Structure: organizing your tests

test() / it() — define a single test

it() is just an alias for test(). Use whichever reads better; many people prefer it() because it makes the sentence flow (“it returns the sum”).

test('adds two numbers', () => {
  expect(add(2, 3)).toBe(5);
});

it('returns the sum of two numbers', () => {
  expect(add(2, 3)).toBe(5);
});

describe() — group related tests

Groups tests into a block so the output is readable and shared setup is scoped.

describe('add', () => {
  test('adds positive numbers', () => {
    expect(add(2, 3)).toBe(5);
  });

  test('handles negatives', () => {
    expect(add(-1, -1)).toBe(-2);
  });
});

Setup and teardown: beforeEach / afterEach / beforeAll / afterAll

beforeEach / afterEach run around every test in scope. beforeAll / afterAll run once for the whole block. Use the “each” variants to reset state between tests; use the “all” variants for expensive one-time setup like a DB connection.

describe('shopping cart', () => {
  let cart;

  beforeEach(() => {
    cart = [];            // fresh state for every test
  });

  afterEach(() => {
    cart = null;          // cleanup
  });

  test('starts empty', () => {
    expect(cart).toHaveLength(0);
  });
});

2. Assertions: expect() and matchers

expect(value) wraps the value you’re testing, then you chain a matcher that describes what you expect. You can negate any matcher with .not.

expect(add(2, 2)).toBe(4);
expect(add(2, 2)).not.toBe(5);

Equality

MatcherUse for
toBe()Strict equality (===) — primitives like numbers, strings, booleans
toEqual()Deep equality — objects and arrays (compares contents)
toStrictEqual()Like toEqual() but also checks types and undefined properties
expect(2 + 2).toBe(4);                          // primitive
expect({ a: 1 }).toEqual({ a: 1 });             // objects — toBe would FAIL here
expect([1, 2]).toEqual([1, 2]);                 // arrays
expect({ a: 1 }).toStrictEqual({ a: 1 });       // strictest

Gotcha: toBe() on two separate objects always fails because they’re different references in memory. Use toEqual() for anything that isn’t a primitive.

Truthiness

expect(true).toBeTruthy();
expect(0).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('hello').toBeDefined();

Numbers

expect(10).toBeGreaterThan(5);
expect(5).toBeLessThan(10);
expect(0.1 + 0.2).toBeCloseTo(0.3);   // float math — never use toBe here

toBeCloseTo() exists because 0.1 + 0.2 === 0.30000000000000004 in JavaScript. Use it for any floating-point comparison.

Strings and arrays

expect('team work').toContain('work');         // substring
expect(['a', 'b', 'c']).toContain('b');        // array membership
expect([1, 2, 3]).toHaveLength(3);
expect('2024-06-22').toMatch(/^\d{4}-\d{2}-\d{2}$/);  // regex

Objects

const user = { name: 'Vishal', roles: { admin: true } };
expect(user).toHaveProperty('name');
expect(user).toHaveProperty('name', 'Vishal');     // key + value
expect(user).toHaveProperty('roles.admin', true);  // nested path

Errors: toThrow()

The most common Jest mistake lives here. You must pass a function, not the result of calling it — otherwise the error throws before Jest can catch it.

// WRONG — throws immediately, test errors out
expect(divide(10, 0)).toThrow();

// RIGHT — wrap in an arrow function so Jest controls when it runs
expect(() => divide(10, 0)).toThrow();
expect(() => divide(10, 0)).toThrow('Cannot divide by zero');  // substring match
expect(() => divide(10, 0)).toThrow(Error);                    // by type

3. Async tests

A function that returns a promise needs special handling, or the test will pass before the promise settles.

async / await

async function fetchUser(id) {
  if (id < 0) throw new Error("Invalid id");
  return { id, name: "Alice" };
}

test('fetches a user', async () => {
  const user = await fetchUser(1);
  expect(user).toEqual({ id: 1, name: 'Alice' });
});

resolves / rejects

A cleaner style for asserting directly on a promise. Don’t forget the await — without it, a failing assertion can slip through silently.

test('resolves to a user', async () => {
  await expect(fetchUser(1)).resolves.toEqual({ id: 1, name: 'Alice' });
});

test('rejects on invalid id', async () => {
  await expect(fetchUser(-1)).rejects.toThrow('Invalid id');
});

4. Mocks

Mocks let you replace real dependencies (network calls, timers, other modules) with fakes you control, and then assert how they were used.

jest.fn() — a standalone mock function

test('calls the callback', () => {
  const callback = jest.fn();

  [1, 2, 3].forEach(callback);

  expect(callback).toHaveBeenCalled();
  expect(callback).toHaveBeenCalledTimes(3);
  expect(callback).toHaveBeenCalledWith(1, 0, [1, 2, 3]);
});

You can also control its return value:

const getId = jest.fn().mockReturnValue(42);
expect(getId()).toBe(42);

const fetchData = jest.fn().mockResolvedValue({ ok: true });
await expect(fetchData()).resolves.toEqual({ ok: true });

jest.spyOn() — watch (and optionally replace) a real method

Useful when you want to track calls to an existing method without rewriting it. Restore it afterward so other tests aren’t affected.

test('logs a warning', () => {
  const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});

  console.warn('careful');

  expect(spy).toHaveBeenCalledWith('careful');
  spy.mockRestore();   // put the real method back
});

jest.mock() — replace an entire module

Mock out a whole dependency, like an HTTP client, so your tests never hit the network.

jest.mock('axios');
const axios = require('axios');

test('fetches users from the API', async () => {
  axios.get.mockResolvedValue({ data: [{ id: 1 }] });

  const res = await axios.get('/users');

  expect(res.data).toEqual([{ id: 1 }]);
  expect(axios.get).toHaveBeenCalledWith('/users');
});

Mock matchers at a glance

expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenLastCalledWith('latest');

Quick reference

CategoryFunctions
Structuredescribetest / itbeforeEachafterEachbeforeAllafterAll
EqualitytoBetoEqualtoStrictEqual
TruthinesstoBeTruthytoBeFalsytoBeNulltoBeUndefinedtoBeDefined
NumberstoBeGreaterThantoBeLessThantoBeCloseTo
Strings/arraystoContaintoHaveLengthtoMatch
ObjectstoHavePropertytoEqual
ErrorstoThrow
Asyncasync/awaitresolvesrejects
Mocksjest.fnjest.spyOnjest.mock
Mock matcherstoHaveBeenCalledtoHaveBeenCalledTimestoHaveBeenCalledWith

Three mistakes worth memorizing: use toEqual (not toBe) for objects and arrays; wrap throwing calls in an arrow function for toThrow; and always await your resolves / rejects assertions.

Introducing My WooCommerce Product Table Plugin 🚀

Over the last few months, I’ve been working deeply with WooCommerce — especially in scenarios where product catalogs are large, complex, and performance-sensitive.

One recurring problem stood out:

👉 The default WooCommerce grid layout breaks down when users need speed, comparison, and bulk actions.

So I built something to solve that properly.

Today, I’m open-sourcing it:

👉 https://github.com/vishalkakadiya/wc-product-table/


💡 The Problem

WooCommerce works great for standard stores.

But when you move into:

  • Wholesale / B2B
  • Bulk ordering systems
  • Course or product catalogs
  • Restaurants / menus

Users don’t want to “browse” — they want to:

✔ Scan quickly
✔ Search instantly
✔ Add multiple items fast

A table UI fits this use case far better than grids.


⚙️ What This Plugin Does

This plugin lets you display WooCommerce products in a fully interactive table with:

  • Search
  • Sorting
  • Filtering
  • Pagination
  • AJAX cart actions

All optimized for speed and usability.


🔥 Key Features

Here are the highlights that make it powerful:

⚡ AJAX Add to Cart (Single + Bulk)

Add products without page reload — including bulk add-to-cart functionality.

🧩 Variable Product Support

Select variations directly inside the table — no need to visit product pages.

🔎 Search, Filter & Sort

  • Real-time search
  • Category & tag filters
  • Sortable columns (price, name, etc.)

📄 Pagination with Control

AJAX-powered pagination with customizable per-page limits.

🔗 External/Affiliate Support

Handles affiliate products cleanly with proper CTA behavior.


🧠 Built for Developers (This Matters)

This is not just a UI plugin — it’s built as a framework-style extension.

  • ✅ 50+ hooks (filters + actions)
  • ✅ Fully customizable columns
  • ✅ Template override system
  • ✅ Extensible query layer
  • ✅ Clean separation of logic and UI

You can literally customize every part of the table — from query → UI → cart behavior.


🛠️ Example: Add a Custom Column

You can extend the table like this:

add_filter( 'wcpt_table_columns', function( $columns ) {
$columns['sku'] = [
'label' => 'SKU',
'sortable' => true,
];
return $columns;
});add_action( 'wcpt_render_column_sku', function( $product ) {
echo $product->get_sku();
});

This level of extensibility is intentional — I wanted this to work well in real-world enterprise setups, not just demos.


⚡ Quick Usage

Just drop the shortcode:

[wc_product_table]

Or customize it:

[wc_product_table per_page="25" category="clothing"]

Simple, flexible, and production-ready.


🎯 Where This Works Best

This plugin is ideal for:

  • Wholesale stores
  • B2B dashboards
  • LMS product/course listings
  • Bulk ordering interfaces
  • High SKU catalogs

🏗️ My Approach While Building This

Since I work as a WordPress architect, I built this with a few strong principles:

  • Performance first
  • Extensibility over shortcuts
  • Clean architecture
  • Real-world usability

This is not a “just make it work” plugin — it’s designed to scale.


🔓 Open Source

The plugin is fully open source.

👉 https://github.com/vishalkakadiya/wc-product-table/

If you want to:

  • Extend it
  • Use it in your projects
  • Or contribute

Feel free to jump in.


🚀 What’s Next

I’m planning to evolve this further with:

  • Advanced filters
  • Gutenberg block support
  • More UI customization
  • Better performance optimizations

💬 Feedback Welcome

GitHub: https://github.com/vishalkakadiya/wc-product-table

If you try it out, I’d love to hear:

  • What works
  • What breaks
  • What you wish it could do

If you’re building serious WooCommerce solutions, you already know:

👉 UX + performance + flexibility = everything

This plugin is built exactly with that mindset.

Mastering the WordPress Abilities API: Fine-Grained Control for Modern WordPress Development

The WordPress Abilities API provides a standardized way to register and discover distinct units of functionality within a WordPress site. These units, called “Abilities”, represent specific actions or capabilities that components can perform, with clearly defined inputs, outputs, and permissions.

It acts as a central registry, making it easier for different parts of WordPress, third-party plugins, themes, and external systems (like AI agents) to understand and interact with the capabilities available on a specific site.

Core Concepts

  • Ability: A distinct piece of functionality with a unique name following the namespace/ability-name pattern. Each ability has a human-readable name and description, input/output definitions (using JSON Schema), a category assignment, optional permissions, and an associated callback function for execution. Each registered Ability is an instance of the WP_Ability class.
  • Category: A way to organize related abilities. Each ability must belong to exactly one category. Categories have a slug, label, and description. Each registered category is an instance of the WP_Ability_Category class.
  • Registry: A central, singleton object (WP_Abilities_Registry) that holds all registered abilities. It provides methods for registering, unregistering, finding, and querying abilities. Similarly, WP_Abilities_Category_Registry manages all registered categories.
  • Callback: The PHP function or method executed when an ability is called via WP_Ability::execute().
  • Schema: JSON Schema definitions for an ability’s expected input (input_schema) and its returned output (output_schema). This allows for validation and helps agents understand how to use the ability.
  • Permission Callback: An optional function that determines if the current user can execute a specific ability.
  • Namespace: The first part of an ability name (before the slash), typically matching the plugin or component name that registers the ability.

Important Resources

  • https://developer.wordpress.org/apis/abilities-api/
  • https://developer.wordpress.org/news/2025/11/introducing-the-wordpress-abilities-api/

What is the difference between select(‘core’) vs apiFetch, and when to use which?

This is a very important architectural decision in Gutenberg — and honestly, many developers misuse both.

Let’s break it down in a practical, real-world way so you can decide like a tech lead 👇


🧠 select('core') vs apiFetch — What’s the Difference?

Featureselect('core')apiFetch
Data SourceWP Data Store (core-data)Direct REST API
Reactivity✅ Reactive (auto re-render)❌ Not reactive
Caching✅ Built-in caching❌ No caching
Performance✅ Optimized⚠️ Depends on usage
Ease of Use✅ Cleaner in blocks⚠️ More manual
Control⚠️ Limited✅ Full control

✅ When You Should Use select('core') (Recommended Most of the Time)

👉 Use this inside Gutenberg blocks/UI

✔ Best for:

  • Fetching posts, pages, users
  • Displaying dynamic data in UI
  • When you want automatic updates
  • When performance matters

Example

const posts = useSelect((select) => {
return select('core').getEntityRecords('postType', 'post');
}, []);

💡 Why it’s better

  • Uses centralized state (Redux-like store)
  • Avoids duplicate API calls
  • Auto re-renders UI when data changes
  • Works seamlessly with Gutenberg

⚠️ When apiFetch is Better

👉 Use this when you need full control or custom behavior

✔ Best for:

  • Custom REST API endpoints
  • POST / PUT / DELETE requests
  • Non-Gutenberg environments
  • One-time operations (form submit, migration scripts, etc.)

Example

import apiFetch from '@wordpress/api-fetch';const fetchData = async () => {
const posts = await apiFetch({
path: '/wp/v2/posts?per_page=5',
}); console.log(posts);
};

🚨 Common Mistake (Very Important)

❌ Using apiFetch inside React render logic:

// ❌ BAD PRACTICE
const posts = await apiFetch({ path: '/wp/v2/posts' });

👉 This causes:

  • Multiple API calls
  • No caching
  • Performance issues

⚡ Real-World Decision Rule (Use This)

👉 Ask yourself:

1. Is this UI data inside Gutenberg?

👉 ✅ Use select('core')


2. Is this a custom API or action (POST/UPDATE)?

👉 ✅ Use apiFetch


3. Do I need caching + reactivity?

👉 ✅ Use select('core')


4. Do I need full control over request?

👉 ✅ Use apiFetch


🏆 Best Practice (Senior-Level Pattern)

👉 Combine both when needed:

  • Use select('core') → for reading data
  • Use apiFetch → for writing/updating data

Example Pattern

// Read (cached + reactive)
const posts = useSelect((select) =>
select('core').getEntityRecords('postType', 'post')
);// Write (manual control)
const createPost = async () => {
await apiFetch({
path: '/wp/v2/posts',
method: 'POST',
data: { title: 'New Post' },
});
};

🧩 Bonus Insight (Interview-Level)

👉 select('core') internally:

  • Uses @wordpress/core-data
  • Wraps REST API calls
  • Adds caching + resolution tracking
  • Prevents duplicate requests

🏁 Final Verdict

👉 If you’re building Gutenberg blocks:

Use select('core') 80–90% of the time

👉 Use apiFetch only when:

  • You need custom logic
  • Or you’re mutating data

A Practical Guide to Fetching Custom Post Types and Taxonomies Using select(‘core’) in Gutenberg

✅ 1. Fetch Custom Post Type (product)

Just replace 'post' with 'product':

const { products, isLoading } = useSelect((select) => {
	const query = {
		per_page: 5,
		_fields: ['id', 'title'],
	};	return {
		products: select('core').getEntityRecords('postType', 'product', query),
		isLoading: !select('core').hasFinishedResolution(
			'getEntityRecords',
			['postType', 'product', query]
		),
	};
}, []);





✅ 2. Fetch Taxonomy (product_cat)

For taxonomies, entity type changes from 'postType''taxonomy'

const { categories, isCatLoading } = useSelect((select) => {
	const query = {
		per_page: 10,
		_fields: ['id', 'name'],
	};	return {
		categories: select('core').getEntityRecords('taxonomy', 'product_cat', query),
		isCatLoading: !select('core').hasFinishedResolution(
			'getEntityRecords',
			['taxonomy', 'product_cat', query]
		),
	};
}, []);





🚀 3. Fetch BOTH (Best Practice – Single useSelect)

👉 This is what you should do in production (avoids multiple subscriptions)

const { products, categories, isLoading } = useSelect((select) => {
	const productQuery = {
		per_page: 5,
		_fields: ['id', 'title'],
	};	const categoryQuery = {
		per_page: 10,
		_fields: ['id', 'name'],
	};	const core = select('core');	return {
		products: core.getEntityRecords('postType', 'product', productQuery),
		categories: core.getEntityRecords('taxonomy', 'product_cat', categoryQuery),		isLoading:
			!core.hasFinishedResolution('getEntityRecords', ['postType', 'product', productQuery]) ||
			!core.hasFinishedResolution('getEntityRecords', ['taxonomy', 'product_cat', categoryQuery]),
	};
}, []);

🔗 4. Filter Products by Category (Very Useful)

If you want products by category, pass taxonomy query:

const query = {
per_page: 5,
product_cat: 12, // category ID
};

👉 Full example:

products: core.getEntityRecords('postType', 'product', {
	per_page: 5,
	product_cat: 12,
})





⚠️ Important Notes

1. CPT must be exposed to REST API

When registering CPT:

'show_in_rest' => true,

2. Taxonomy must also support REST

'show_in_rest' => true,

3. _fields optimization

Be careful:

_fields: ['id', 'title']
  • Works for posts
  • For taxonomy → use name, not title

🧠 Key Takeaways

  • CPT → 'postType', 'product'
  • Taxonomy → 'taxonomy', 'product_cat'
  • Combine in single useSelect for performance
  • Use hasFinishedResolution for loading state
  • Pass taxonomy slug in query to filter posts