The plugin object said provides.skills was 2. The loaded skill list had one name, inside-skill. The missing directory was a symlink. Its target was an ordinary skill directory, the SKILL.md there was readable, and Grok 1.0.67 did not print a skip.
That pair of numbers is not the install result. grok plugin install --trust on a plugin whose only skill was the same kind of outside link exited 0 and said it installed the plugin. The installed copy had an empty skills directory. A later grok inspect --json of that home reported provides.skills as 0 and an empty skill list. grok plugin details on the same installed copy still said 1 skill dir(s).
Three commands, three counts, and none of them said the link had been left out because its target was outside the plugin directory. The 1.0.70 changelog says the /plugins view now counts only skills that load and says how many were skipped for that reason. This note did not install 1.0.70 and did not open that view. The numbers below are the 1.0.67 headless commands.
What was on disk
The binary was Grok Build 1.0.67, build 2fe064390d07, on Linux x86_64. The run was 4 November 2026. Each case used a new GROK_HOME. The normal grok home was not the directory under test. No grok login was run. No prompt was sent. The commands were grok plugin validate, grok plugin install --trust, grok plugin details, grok inspect --json, and grok update --check --json.
grok update --check --json reported currentVersion 1.0.67, latestVersion 1.0.70, and updateAvailable true. The check did not replace the binary. The user install’s binary timestamp was unchanged after the lab.
Each plugin was a directory with plugin.json and a skills directory. The manifest named the plugin, set version to 0.0.1, and set a one-line description. A skill was a subdirectory holding SKILL.md. The front matter set name and description. The body was one sentence telling the agent not to run tools. Nothing in these files was fetched, and nothing in them was a hook or an MCP server.
The in-place cases were a copy of that directory under <grok-home>/plugins/<plugin-name>. The install cases were grok plugin install --trust of the source directory, which writes the installed tree under installed-plugins/ and writes the plugin name into [plugins].enabled. Those are different directories. Inspect reads the one that is actually present. When both a plugins/ entry and an installed copy exist, the skill path in the JSON was the plugins/ path, including after that entry was renamed. Do not assume the installed tree is the tree the skill list just walked.
The plugins chapter written into the disposable home by this binary says plugins stay off until enabled, and that a hands-off copy under ~/.grok/plugins/ still needs [plugins].enabled. It says grok inspect lists every discovered plugin and the skills it provides. It does not say that provides.skills can be larger than the skill list, and it does not say that a skill link whose target leaves the plugin directory is omitted from that list.
Enabled is already true before a name is listed
A plugin with two ordinary skill directories, alpha and beta, and no [plugins] table, was still reported enabled: true. provides.skills was 2. The skill list was empty.
The same files with this line in <grok-home>/config.toml loaded both names:
[plugins]
enabled = ["two-plugin"]
provides.skills stayed 2. The skill list was alpha and beta. Install of that same source wrote the same enable line by itself, copied both SKILL.md files, and inspect of the installed copy loaded both names. The enable line is the switch that fills the skill list. The enabled field on the plugin object is not that switch. It was true before the name was listed, and it was true after.
An empty skill list therefore has two causes on this binary, and they look the same if you only read the list. If the name is absent from [plugins].enabled, every skill is absent, including skills that are ordinary directories inside the plugin. If the name is present and the count is still higher than the list, the gap is not the enable switch. The outside link is that second gap.
A link inside the plugin loads twice
The control for “any symlink” was a skill directory alias-skill whose target was kept, a real skill directory in the same skills folder. The link did not leave the plugin. With the plugin name in [plugins].enabled, inspect loaded two skill objects. Both were named kept, which is the front matter name, not the directory name alias-skill. One path was skills/alias-skill/SKILL.md. The other was skills/kept/SKILL.md. provides.skills was 2.
A symlink is not what the loader refuses. A link that stays inside the plugin is loaded, and it is loaded under the name in the file, so the skill list can contain the same name twice. If you later disable one path by name, you are disabling both, because the name came from the front matter of the shared file.
A link that leaves the plugin is counted and omitted
The outside directory was the same shape of skill, with its own SKILL.md, stored beside the plugin rather than under it. Three resolving links were measured after the plugin name was enabled.
An absolute symlink from skills/dir-skill to that outside directory produced provides.skills of 1 and an empty skill list. The target file was present. A relative symlink, ../../../outside-target/rel-skill, resolved outside the plugin directory and still inside the disposable home. It also produced provides.skills of 1 and an empty skill list. A skill directory that itself stayed inside the plugin, with only SKILL.md as a symlink to an outside file, produced the same pair: count 1, loaded list empty. The file command on that path reported a symbolic link, and the referent was the outside SKILL.md.
A plugin that held both an inside skill and an absolute outside directory link is the case that makes the gap obvious. After enable, provides.skills was 2 and the skill list contained only inside-skill. The outside name never appeared. The JSON has a permissions.skipped array. On these runs it was empty, and it is not a skill-skip list. There is no skill-level skipped field in the inspect object this binary printed.
A relative link that did not resolve was a different number. After a copy that preserved the link text but not the relative target, provides.skills was 0 and the skill list was empty. A broken link is not counted. A link that resolves, and whose target is outside the plugin directory, is counted and then omitted from the list. The count of 1 is how you tell those two apart. It is not evidence that the skill loaded.
The same outside directory link, passed to grok plugin install --trust instead of left under plugins/, did not stay as a link. The command exited 0. The human line was Installed 1 plugin(s) and the plugin name. The installed tree contained plugin.json and a skills directory with no entries. Inspect of that home reported provides.skills 0, an empty skill list, enabled true, and a plugin path under installed-plugins/. The source directory still had the symlink. The installed copy did not, and the success line did not say the skill had been dropped.
That is why an install can look cleaner than the source. The count falls to 0 because the link was not copied, not because the loader accepted the skill and then found nothing to do. grok plugin details on that installed copy still printed components: 1 skill dir(s). The empty directory is enough for that sentence.
Validate counts the container
grok plugin validate on the two-skill plugin and on the outside-link plugin printed the same component line:
Plugin manifest is valid.
components: 1 skill dir(s), 0 command dir(s), 0 agent dir(s)
Both exited 0. The two-skill plugin has two skill directories. The outside-link plugin has one, and it is a symlink. The number 1 is the skills directory, not the number of skills inside it, and it does not change when that directory’s only entry points outside the plugin. Details after the install that dropped the link used the same sentence. A passing validate does not mean the skill list will contain those skills, and it does not mean install will copy them.
The chapter’s instruction to validate before publishing is still the right check for a broken manifest. It is the wrong check for this gap. The manifest was valid in every case above, including the case install reduced to an empty skills directory.
Verify the count against the skill list
Point GROK_HOME at the home you mean to test, or accept that the command reads the default home. Then confirm the plugin name is in [plugins].enabled before you treat an empty skill list as a skipped link. On this binary the plugin object says enabled true either way. The skill list is the field that changes when you add the name.
grok inspect --json
In the plugin object, read provides.skills. In the top-level skills array, count objects whose source.plugin_name is that plugin. If those two numbers match, the outside-link gap is not what this binary showed for that directory. If provides.skills is higher, list skills/ in the plugin path the JSON printed, not in a different copy.
A directory that is a symlink, or a SKILL.md that is a symlink, needs its target resolved. If the resolved path is outside the plugin directory and the file is readable, this 1.0.67 binary counted it and did not load it. If the link does not resolve, the count did not include it. An inside link is loaded under the front matter name, which can duplicate a skill that is also present as a real directory.
If the plugin arrived through grok plugin install, inspect the installed path from the JSON before you inspect the source. A source that still has the outside link, and an installed tree whose skills directory is empty, are the before and after of a copy that dropped the link without saying so. grok plugin details printing 1 skill dir(s) does not contradict that empty directory.
The Grok Build changelog entry for 1.0.70, read 4 November 2026, says /plugins now counts only the plugin skills that load, and says how many were skipped because they link outside the plugin folder. grok update --check --json on this host named 1.0.70 as the latest stable version. That binary was not installed here, and /plugins was not opened. Do not treat the changelog sentence as a description of the inspect JSON this note measured, and do not treat an upgrade you have not run as proof that the JSON field changed. If you do install 1.0.70, compare the same directory before and after, and read both the modal and grok inspect --json. A modal that reports the skip does not, by itself, show whether the headless count still includes the link.
No prompt was sent, so this run does not show whether a later session can read the outside file through a tool that is not skill discovery. The loader’s omission is not a statement that the file is unreachable. It is a statement that the skill list and the plugin skill count disagree, and that install can remove the disagreement by dropping the link quietly.
Sources
- Grok Build 1.0.67, build
2fe064390d07, Linux x86_64, 4 November 2026. DisposableGROK_HOMEfor each case. No login and no prompt. Commands weregrok plugin validate,grok plugin install --trust,grok plugin details,grok inspect --json, andgrok update --check --json. grok update --check --jsonon that binary:currentVersion1.0.67,latestVersion1.0.70,updateAvailabletrue. The check did not replace the installed binary.- Plugins chapter written into the disposable home by Grok 1.0.67,
docs/user-guide/09-plugins.md: plugins stay off until listed in[plugins].enabled; a copy under the user plugins directory still needs that list;grok inspectis the command that lists discovered plugins and the skills they provide. - Grok Build changelog, 1.0.70 entry, read 4 November 2026. The
/pluginsskip sentence is the line this run did not execute.