PHPackages                             malikad778/wp-hook-check - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Testing &amp; Quality](/categories/testing)
4. /
5. malikad778/wp-hook-check

ActiveLibrary[Testing &amp; Quality](/categories/testing)

malikad778/wp-hook-check
========================

Detect orphaned and unheard WordPress hooks via static analysis. No WordPress installation required.

v1.0.1(2mo ago)100MITPHPPHP ^8.2CI passing

Since Feb 25Pushed 2mo ago1 watchersCompare

[ Source](https://github.com/malikad778/wp-hook-check)[ Packagist](https://packagist.org/packages/malikad778/wp-hook-check)[ Docs](https://github.com/malikad778/wp-hook-check)[ RSS](/packages/malikad778-wp-hook-check/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (2)Dependencies (5)Versions (3)Used By (0)

wp-hook-check
=============

[](#wp-hook-check)

**Static analysis for WordPress hooks. Find broken hook connections before they reach production.**

[![Tests](https://github.com/malikad778/wp-hook-check/actions/workflows/tests.yml/badge.svg)](https://github.com/malikad778/wp-hook-check/actions)[![Latest Version](https://camo.githubusercontent.com/6eb8dc36a4b83295e5865220a9fc284b078eefebe3e23a923e99b333465ec2d2/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d616c696b61643737382f77702d686f6f6b2d636865636b)](https://packagist.org/packages/malikad778/wp-hook-check)[![PHP Version](https://camo.githubusercontent.com/b1a203e283b53ee0aba782d29cb9eb8106b36f4bd230bc6633f302ed505b47be/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f6d616c696b61643737382f77702d686f6f6b2d636865636b)](https://packagist.org/packages/malikad778/wp-hook-check)[![License: MIT](https://camo.githubusercontent.com/08cef40a9105b6526ca22088bc514fbfdbc9aac1ddbf8d4e6c750e3a88a44dca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d626c75652e737667)](LICENSE)

[![Demo](demo.gif)](demo.gif)

---

The problem
-----------

[](#the-problem)

WordPress hooks are silent. When `add_action('my_hook', $cb)` exists but `do_action('my_hook')` was renamed or removed, nothing throws. The callback just stops running. You find out in production when a feature breaks.

```
// v1 - fires a hook before checkout
do_action( 'my_plugin_before_checkout', $order_id );

// Another plugin listens for it
add_action( 'my_plugin_before_checkout', 'apply_discount' );

// v2 - renamed without announcement
do_action( 'my_plugin_checkout_start', $order_id );
// apply_discount() never runs again. No error, no warning.
```

This tool parses every PHP file in a directory, maps all hook registrations and invocations, and reports mismatches. No WordPress install needed.

---

Install
-------

[](#install)

### Package (Composer)

[](#package-composer)

```
composer require --dev malikad778/wp-hook-check
```

PHP 8.2+.

### Global (WP-CLI)

[](#global-wp-cli)

Install globally via WP-CLI to scan any site:

```
wp package install malikad778/wp-hook-check
```

---

Usage
-----

[](#usage)

### As a standalone script

[](#as-a-standalone-script)

```
# Scan current directory
vendor/bin/wp-hook-audit audit .
```

### Via WP-CLI

[](#via-wp-cli)

```
# Scan a specific plugin
wp hook-check ./wp-content/plugins/my-plugin
```

Scan a plugin
=============

[](#scan-a-plugin)

vendor/bin/wp-hook-audit audit ./wp-content/plugins/my-plugin

Scan all plugins at once (hooks are cross-referenced across all files)
======================================================================

[](#scan-all-plugins-at-once-hooks-are-cross-referenced-across-all-files)

vendor/bin/wp-hook-audit audit ./wp-content/plugins

```

---

## What gets flagged

| Type | Severity | When |
|---|---|---|
| `ORPHANED_LISTENER` | 🔴 HIGH | `add_action/filter` exists, no matching `do_action/apply_filters` found |
| `UNHEARD_HOOK` | 🟡 MEDIUM | `do_action/apply_filters` fired, no listener registered anywhere |
| `HOOK_NAME_TYPO` | 🔴 HIGH | Hook name differs from another by 1–2 characters |
| `DYNAMIC_HOOK` | 🔵 INFO | Hook name is a variable - can't be resolved, skipped by other detectors |

---

## Output formats

### Table (default)

```

WP HOOK AUDITOR Scanned 47 files in 0.091s ──────────────────────────────────────────────────────────

\[HIGH\] ORPHANED\_LISTENER File : includes/class-checkout.php:234 Hook : my\_plugin\_before\_checkout

add\_action('my\_plugin\_before\_checkout') registered (callback: apply\_discount)

- no matching do\_action() or apply\_filters() found.

Fix: Either remove the add\_action() call or add do\_action('my\_plugin\_before\_checkout') where it should fire.

────────────────────────────────────────────────────────── SUMMARY 1 HIGH 0 MEDIUM 0 INFO

```

### JSON (`--format=json`)

```json
{
  "meta": {
    "files_scanned": 47,
    "duration_sec": 0.091,
    "issue_count": 1
  },
  "issues": [
    {
      "type": "orphaned_listener",
      "severity": "high",
      "hook": "my_plugin_before_checkout",
      "file": "includes/class-checkout.php",
      "line": 234,
      "message": "...",
      "safe_alternative": "...",
      "suggestion": null
    }
  ]
}

```

### GitHub Annotations (`--format=github`)

[](#github-annotations---formatgithub)

```
::error file=includes/class-checkout.php,line=234,title=ORPHANED_LISTENER::...
::warning file=...,title=UNHEARD_HOOK::...
::notice file=...,title=DYNAMIC_HOOK::...

```

Issues show up as inline annotations on the exact lines in GitHub pull requests.

---

CLI options
-----------

[](#cli-options)

### `audit` / `wp hook-check`

[](#audit--wp-hook-check)

```
vendor/bin/wp-hook-audit audit [path] [options]
# OR
wp hook-check [path] [options]
```

OptionDefaultDescription`--format``table``table`, `json`, or `github``--fail-on``high`Exit 1 if issues at this level exist: `high`, `medium`, `any`, `none``--exclude`-Comma-separated paths to skip`--ignore-dynamic`-Hide INFO dynamic hook notices`--only`allRun only these detectors: `orphaned`, `unheard`, `typo`, `dynamic``--config``wp-hook-audit.json`Path to config file### `dump`

[](#dump)

```
vendor/bin/wp-hook-audit dump [path] [--format=table|json]
```

Dumps the full hook map - every `add_action`, `do_action`, `add_filter`, `apply_filters` call, with file, line, and priority. No detectors run. Good for exploring an unfamiliar codebase. *(Not currently supported via WP-CLI).*

---

Exit codes
----------

[](#exit-codes)

CodeMeaning`0`Clean (no issues above threshold)`1`Issues found at or above `--fail-on` level`2`Parse error, unreadable file, or bad config---

Config file
-----------

[](#config-file)

Drop a `wp-hook-audit.json` in the directory you're scanning, or point to one with `--config`:

```
{
    "exclude": ["vendor/", "tests/", "node_modules/"],
    "detectors": {
        "orphaned_listener": true,
        "unheard_hook": true,
        "typo": true,
        "dynamic_hook": false
    },
    "ignore": [
        { "type": "unheard_hook", "hook": "my_plugin_extensibility_point" }
    ],
    "external_prefixes": [
        "wp_", "admin_", "woocommerce_", "init", "shutdown"
    ]
}
```

### `external_prefixes`

[](#external_prefixes)

WordPress core fires hundreds of hooks (`init`, `plugins_loaded`, `save_post`, etc.) that live inside WordPress itself, not your plugin. Without this setting, every `add_action('init', ...)` flags as `ORPHANED_LISTENER` because the matching `do_action('init')` is in WordPress core - outside the folder you're scanning.

The defaults already cover 40+ common WP core patterns. Add your own plugin's extensibility hooks here too if you're getting false positives from a third-party plugin you depend on.

See `wp-hook-audit.json.example` for the full default list.

---

CI/CD
-----

[](#cicd)

### GitHub Actions

[](#github-actions)

```
name: Hook Audit
on: [pull_request]

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with: { php-version: '8.2' }
      - run: composer require --dev malikad778/wp-hook-check
      - run: vendor/bin/wp-hook-audit audit . --format=github --fail-on=high
```

### GitLab CI

[](#gitlab-ci)

```
hook_audit:
  script:
    - composer install
    - vendor/bin/wp-hook-audit audit . --format=json > hook-report.json
  artifacts:
    paths: [hook-report.json]
```

---

How it works
------------

[](#how-it-works)

Parses PHP files into an AST using `nikic/php-parser`, walks every function call node, and records any of the 10 tracked WordPress functions. Hook names are extracted from the first argument - string literals are captured as-is, variables and concatenations are marked dynamic and skipped by mismatch detectors. The result is a `HookMap` keyed by hook name, which the four detectors then query.

Parse errors are non-fatal. A file that fails to parse is skipped with a warning, scan continues.

---

Tracked functions
-----------------

[](#tracked-functions)

FunctionRole`add_action()`, `add_filter()`Registration - checked for orphaned listeners`do_action()`, `apply_filters()`Invocation - checked for missing listeners`do_action_ref_array()`, `apply_filters_ref_array()`Invocation`remove_action()`, `remove_filter()`Tracked, never flagged`has_action()`, `has_filter()`Counts as invocation - stops false UNHEARD positives---

Known gaps
----------

[](#known-gaps)

- Dynamic hook names (variables, string concatenation) are skipped by all mismatch detectors
- Hooks registered inside conditionals are still tracked - may produce false positives if the condition never runs
- Closures show as `{closure}` in the hook map output
- Hooks from WordPress core or third-party plugins need their prefixes in `external_prefixes`

---

License
-------

[](#license)

MIT - see [LICENSE](LICENSE)

###  Health Score

38

—

LowBetter than 85% of packages

Maintenance85

Actively maintained with recent releases

Popularity6

Limited adoption so far

Community4

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

80d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/e4a7f00f437e6ae1436953cf64cd6fa1d55a13da6b9c369340b2cfd8ddd4ef42?d=identicon)[malikad778](/maintainers/malikad778)

---

Tags

automated-testingclicode-qualitydeveloper-toolslintingpestphpphpstatic-analysiswordpresswordpress-developmentwordpress-hookswordpress-pluginscliwordpressstatic analysisAudithookswpdeveloper-tools

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/malikad778-wp-hook-check/health.svg)

```
[![Health](https://phpackages.com/badges/malikad778-wp-hook-check/health.svg)](https://phpackages.com/packages/malikad778-wp-hook-check)
```

###  Alternatives

[vimeo/psalm

A static analysis tool for finding errors in PHP applications

5.8k77.5M6.7k](/packages/vimeo-psalm)[maglnet/composer-require-checker

CLI tool to analyze composer dependencies and verify that no unknown symbols are used in the sources of a package

99810.9M671](/packages/maglnet-composer-require-checker)[infection/infection

Infection is a Mutation Testing framework for PHP. The mutation adequacy score can be used to measure the effectiveness of a test set in terms of its ability to detect faults.

2.2k26.2M1.8k](/packages/infection-infection)[phan/phan

A static analyzer for PHP

5.6k11.2M1.1k](/packages/phan-phan)[magento/magento2-functional-testing-framework

Magento2 Functional Testing Framework

15511.5M30](/packages/magento-magento2-functional-testing-framework)[acquia/orca

A tool for testing a company's software packages together in the context of a realistic, functioning, best practices Drupal build

32902.4k](/packages/acquia-orca)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
