Working on a site someone else built
Reading an Unfamiliar Codebase With an Agent
How to make an agent map a WordPress codebase you did not write, and how to check its reading against the running site before you believe a word of it.
The site arrives as a zip file with a theme, four plugins and a note that says "the footer is custom". You open the theme, find eleven PHP files, and within a minute you are reading someone else's abstraction over a framework you know well. This is the work most WordPress developers actually do, and it is the work an agent is worst at doing unsupervised, because the shape of a codebase that somebody else built is exactly the thing a model will guess at.
Point an agent at an unfamiliar repository and ask what it does, and you will get a fluent summary of the file names. The summary is not reading. The agent has no memory of running this site, no idea which plugin is active, and no way to tell a function that is called on every page from one that was abandoned in 2019. Those are the facts that decide whether your change works, and they are available to you in about ten minutes of commands.
The method in this lesson is a loop. You build the map yourself with cheap commands, you ask the agent to extend and explain it, and then you settle every disagreement by asking the running install. The install is the only party in that conversation that cannot be wrong.
Read the codebase in order of what can be reached
File order is arbitrary. The order that matters is reachability: WordPress calls your code through hooks, and a function that no hook and no template can reach might as well not exist. Almost every WordPress codebase announces its entry points in a form you can grep.
cd wp-content/themes/acme-studio
find . -type f | sort
grep -rn "add_action\|add_filter" --include=*.php .
grep -rn "do_action\|apply_filters" --include=*.php .
On a small agency child theme that produced this:
$ find . -type f | sort
./functions.php
./inc/brand.php
./style.css
./theme.json
$ grep -rn "add_action\|add_filter" --include=*.php .
./functions.php:10:add_action( 'wp_footer', 'acme_studio_footer_note' );
./inc/brand.php:6:add_action( 'wp_enqueue_scripts', 'acme_studio_brand_styles' );
Four files and two registrations. You now know more about this theme than an agent will tell you from a prompt, because you know how little there is. The second grep is the one people skip: a theme that consumes a filter is telling you which plugin is supposed to be feeding it, and a theme that fires its own action is telling you where a future change is expected to attach.
A callback name is a claim about a function that may not exist
Here is the part agents get wrong quietly. Registrations reference callbacks by string, and
PHP resolves those strings at call time. Nothing in the file that registers
acme_studio_footer_note proves that a function with that name is ever defined,
because the definition could be in a file that is never included, in a plugin that is not
active, or nowhere at all.
In the theme above, functions.php includes one file and registers one hook.
inc/brand.php defines both the palette function and the callback it registers. But
the codebase also references acme_studio_register_notice_block and
acme_studio_append_notice in a file that has since been deleted, which is the
normal way a real site accumulates dead references. Cross-checking every callback name against
every definition is tedious by hand and trivial as a probe.
<?php
/**
* Probe file: run with wp eval-file on the site you are reading.
*/
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$symbols = array(
'acme_studio_brand_styles',
'acme_studio_brand_palette',
'acme_studio_register_notice_block',
'acme_studio_append_notice',
'acme_brand_get_config',
);
foreach ( $symbols as $symbol ) {
printf( "%-34s %s\n", $symbol, function_exists( $symbol ) ? 'defined' : 'NOT DEFINED' );
}
printf( "active plugins: %s\n", implode( ', ', (array) get_option( 'active_plugins' ) ) ?: '(none)' );
printf( "stylesheet: %s, template: %s\n", get_stylesheet(), get_template() );
Run against the install this lesson was written on, that file printed the following, and the two lines it did not print are the two names that matter.
$ wp eval-file /tmp/m5-inventory.php
acme_studio_brand_styles defined
acme_studio_brand_palette defined
acme_studio_register_notice_block NOT DEFINED
acme_studio_append_notice NOT DEFINED
acme_brand_get_config NOT DEFINED
active plugins: (none)
stylesheet: acme-studio, template: twentytwentyfive
Three findings, all of which change how you work. The notice block registration refers to a
function that does not exist anywhere, so that file is dead or the contribution is incomplete.
The theme is a child of Twenty Twenty-Five, so any parent template you touch is overwritten by
the next parent update. And there are no active plugins, which means every
function_exists() guard that checks for a plugin is currently taking the negative
branch.
Probe the site, not the source you were handed
The probe above is worth generalising, because it answers the four questions that come up on every unfamiliar site. What is the active theme, and does it have a parent? Which plugins are active? Which of the names this code calls actually exist right now? What state is in the database that the code reads?
WP-CLI answers the first three directly, and the answers are cheap enough to collect before you read a single line of logic.
wp theme list --status=active
wp plugin list --status=active
wp eval-file /tmp/m5-inventory.php
wp option get acme_studio_notice
$ wp plugin list --status=active
name status update version
(no output)
$ wp option get acme_studio_notice
(no output, exit code 1)
The empty wp plugin list --status=active output is a fact about the site, not a
failed command. So is the missing option. Both are the kind of thing an agent will narrate as
"the plugin provides brand configuration" if you let it summarise the code without running it,
and both are the reason a later change breaks: the guard clause in the theme is currently the
only branch that executes.
One caution about which tool answers which question. wp eval-file loads WordPress
first, so a theme that fatals during load takes the probe down with it. That is useful
information, but it means a probe is not a substitute for fetching the site. If the probe dies,
you have found the first thing to fix and you have not yet read any application logic.
Hand the agent the map, then give it one task at a time
With the inventory in hand, an agent becomes useful, and the way to keep it useful is to decide what it is allowed to do. Reading a codebase is a task where the agent has no way to check itself, so the useful instruction is not "explain this theme" but "here are the five registrations I found and the two symbols that do not resolve; tell me which files can reach each callback, and mark anything you cannot trace to a definition as unknown".
The distinction that carries the weight is between explaining and changing. An agent asked to explain produces paragraphs you can check against the inventory. An agent asked to fix produces a diff on a site you have not finished reading, which is how a two-line change becomes a migration. If you want both, ask for the explanation first and make the edit a separate, reviewed step with its own commit.
Write the site facts down before you touch anything
One paragraph, committed to the repository, listing the active theme and its parent, the active plugins, the WordPress version, and the option or table that holds the data your change will read. Every later question in this module is answered against that paragraph, and the agent can be given it as context instead of being asked to rediscover it.
Pick a WordPress site you did not build. It can be a site of your own that you have not opened in a year, a plugin from the directory, or the theme installed on a client site you have approval to inspect. Do the following without editing anything.
- List the files, then grep for
add_action,add_filter,do_actionandapply_filters. Keep both outputs. - Write a probe file that loops over every callback name you found plus every helper
those callbacks call, and prints
definedorNOT DEFINEDfor each. Run it withwp eval-file. - Collect the active theme, its parent, the active plugins and the WordPress version.
- Find one symbol that is referenced but not defined, or one option that the code reads and the database does not have. Write down which of the two it is.
Then write the one-paragraph site summary, and ask the agent to answer this question from the inventory alone: which callbacks run on a normal front-end page, and which do not?
Show the reasoning and the answer to the question
The inventory is the whole answer, and the reason it works is that WordPress only runs a callback if two things are true: the hook fires on that request, and the callback name resolves to a function. Your probe settled the second. The first is settled by knowing which hooks fire on which kind of request, and that does not need an agent.
On the theme in this lesson, wp_enqueue_scripts fires on every front-end
request, so acme_studio_brand_styles runs. wp_footer fires on every
front-end request that reaches the footer, so acme_studio_footer_note runs, and
it is the callback that calls acme_studio_brand_config(), which returns
null while the agency toolkit plugin is inactive. That function is the site's
latent fatal, and no amount of reading tells you that; running it does.
The two NOT DEFINED names do not run at all. acme_studio_notice
is absent from the database, the block is never registered, and the filter that would append
the notice is never attached. A change that assumed the notice feature existed would be built
against a surface that does not execute.
$ wp eval 'var_export( acme_studio_brand_config() );'
NULL
Two answers to the question are defensible, and they differ in what they assume about the environment. If you answer from the code alone, every registered callback whose function exists runs, and everything else is dead code. If you answer against the running site, you can add that one of the reachable callbacks currently produces a fatal because its dependency is missing. The second answer is the one worth keeping, because it is the one that survives contact with the site.
The wrong instinct is to ask the agent which callbacks run. That is a question about the
runtime, and the runtime is a command away. An agent asked to guess will produce a confident
answer that is right about init and wrong about everything specific to this
install.
Official reference
The WordPress developer reference for what this lesson uses.
Progress
Mark this lesson done and it stays marked in this browser.