Files
holocron/plugins/git/skills/git-submodules/SKILL.md
Defame1297 38f1ba4e03 fix(kyberforge): bridge apm content to Claude Code's flat plugin discovery
Claude Code's (and Copilot's) native plugin installer has zero awareness of
.apm/ nesting -- it convention-scans only flat skills/, agents/, commands/,
hooks.json at each plugin's root. Confirmed via strings on the installed
claude binary and live installs of git@holocron/gitea@holocron/kyberforge@
holocron, all reporting Skills(0) Agents(0) Hooks(0) post ADR-0015's apm
conversion. Root cause (apm_cli/core/plugin_manifest.py): apm's plugin.json
compiler deliberately strips skills/agents/commands keys, assuming the host
already auto-discovers those convention directories -- it has no model of
.apm/ being host-visible at all. Separately, apm's own bundle exporter
(apm_cli/bundle/plugin_exporter.py, behind `apm pack --format plugin`)
implements the correct .apm/ -> flat mapping, but only ever targeted
build/<name>-<version>/, a path nothing in marketplace.json's source: points
at.

scripts/sync-plugin-content.sh wraps that bundle exporter and copies its
agents/, skills/, commands/, instructions/, extensions/, and merged
hooks.json back into each plugin's own root as a second tracked
compiled-output category -- same governance status as
.claude-plugin/plugin.json: generated from .apm/, never hand-edited. tests/
subdirectories are excluded from the mirror (dev fixtures, not host-visible
runtime content; several hardcode a relative repo-root walk-up sized for the
.apm/-nested depth, which breaks when duplicated one level shallower).
Applied for real across all 6 plugins and verified two ways: `claude plugin
validate --strict` passes on every real plugin directory, and a live
`claude --plugin-dir <path> -p "list skills/agents"` behavioral test
confirms content is now actually discovered.

Also, from the same issue #90 review round:
- scripts/check-manifests.sh pointed at each plugin's root-level plugin.json
  (checking skills/hooks/mcpServers/agents pointer fields) -- that file was a
  stale near-duplicate of .claude-plugin/plugin.json nothing else read or
  wrote, now deleted across all 6 plugins. check-manifests.sh is rewritten to
  validate .claude-plugin/plugin.json instead, and drops the pointer-field
  checks entirely (nothing to check -- those fields are correctly absent by
  design). Content-presence drift is now check-plugin-content-sync's job, a
  new pre-push hook wired in .pre-commit-config.yaml.

docs/adr/0017 records the root cause and decision in full, including two
rejected alternatives (patching plugin.json's path fields directly -- apm's
compiler strips them on every run; pointing marketplace.json at apm pack's
build/ output -- a version-suffixed non-source directory nothing can install
from without an extra build step). ADR-0015 and CONTEXT.md are updated to
point at it.

Refs: #90
2026-08-13 16:59:03 +00:00

7.6 KiB

name, description, metadata
name description metadata
git-submodules Use when managing Git submodules: add dependencies as submodules, initialize and update nested repositories, sync URLs, inspect status (including detached HEAD and divergence), and safely remove submodules. Handles multi-repo projects with pinning, parallel operations, and recursive traversal. Use for both initial setup and ongoing maintenance workflows, even if the user doesn't explicitly say "submodule". Do not use for general git operations outside of submodule management.
category source_keys
git
git-scm-submodule-docs

Concept

A submodule is a full Git repository embedded as a subdirectory inside a parent repository (the superproject). The superproject doesn't store the submodule's files — it stores a pointer to a specific commit SHA in the submodule's own history, and the two repos keep fully independent commit histories.

Two files govern a submodule, and they serve different audiences:

  • .gitmodules — version-controlled, shared with collaborators. Defines each submodule's name, path, and canonical URL.
  • .git/config — local only, populated by git submodule init. This is where local URL overrides live (e.g. a private mirror) — they never propagate to other clones.

The submodule's own .git directory lives at .git/modules/<name>/ in the superproject, linked to the submodule's working tree via a .git pointer file. After git submodule update, the working tree normally ends up in detached HEAD state — see Gotchas.

Gotchas

  • Detached HEAD by default. git submodule update checks out a specific commit, not a branch. Work on a branch first, then update the pointer in the superproject. Commits made in detached state are invisible until pinned.
  • Two pushes required, in order. Always commit and push the submodule first, then update and push the superproject's pointer. The superproject only stores a commit SHA — if that SHA isn't reachable on the submodule's remote yet, git submodule update fails for anyone who pulls the superproject before the submodule push lands.
  • --recursive is not default. Most commands operate one level deep. Pass --recursive explicitly for nested submodules.
  • .git/modules/ persists after git rm. Manual cleanup is needed: rm -rf .git/modules/<name>/.
  • Detached HEAD detection. Status prefix + means the checked-out commit differs from the superproject's recorded commit — normal after update --remote, but should be re-pinned before committing.
  • Relative URLs resolve against the remote, not the filesystem. A ../foo.git entry in .gitmodules is relative to the superproject's default remote URL.
  • Custom update commands are security-gated. A .gitmodules entry of update = !some-command is never copied to .git/config by git submodule init — this stops a clone from silently executing arbitrary code.

Conventions

  • Use rtk git for parent-repo operations. Drop into the submodule directory only for submodule-specific git commands (committing/pushing inside the submodule itself) — mixing the two from the wrong working directory targets the wrong repo's history.
  • Check for a dirty submodule before committing the parent pointer. After adding or updating a submodule, run git status in both the parent and the submodule. A -dirty suffix means the submodule has uncommitted local changes; committing the parent pointer now would pin a state no one else can reproduce, since those changes exist only in the local working tree.

Operations

  • Clone a repo that has submodules: rtk git clone --recurse-submodules <url> (one step, Git 2.13+) or rtk git clone <url> followed by rtk git submodule update --init --recursive.
  • Add a submodule: rtk git submodule add <url> <path> (-b <branch> to track a branch instead of a pinned commit, --depth 1 for a shallow clone, -f to force past a gitignored path or name conflict, --name <name> when the logical name should differ from the path). Stages a .gitmodules entry and a gitlink — a commit is still required.
  • Initialize: rtk git submodule init [<path>...] copies submodule URLs from .gitmodules to .git/config. This is the point at which local URL overrides can be edited before fetching. Does not clone — use update (or update --init to run both in one step).
  • Update (clone + checkout): rtk git submodule update --init --recursive is the common case — checks out the recorded commit in detached HEAD. Add --remote --merge (or --remote --rebase) to track the branch tip instead, --jobs <n> for parallel clones, -f to discard local changes. Full flag table: references/submodules.md.
  • Inspect status: rtk git submodule status --recursive (add --cached to show SHAs in the superproject index instead of the working tree). Status prefixes: - not initialized, + diverged from the superproject's recorded commit, U merge conflict.
  • Sync and rebind URLs: rtk git submodule sync --recursive after an upstream URL rename propagates .gitmodules changes into .git/config. rtk git submodule set-url <path> <url> changes a URL directly; rtk git submodule set-branch -b <branch> <path> sets the tracking branch used by update --remote.
  • Override a submodule URL locally (private mirror): local-only, doesn't propagate to collaborators, and gets overwritten by the next sync. Full steps: references/submodules.md.
  • Run a command across all submodules: rtk git submodule foreach --recursive '<command>'. Shell variables available inside <command> ($name, $sm_path, $displaypath, $sha1, $toplevel): references/submodules.md.
  • Deinit (unregister without removing): rtk git submodule deinit <path> (--all for every submodule, -f if local modifications are present) clears the .git/config section and empties the working tree. deinit is not removal — the .gitmodules entry and the gitlink in the superproject's index are untouched.
  • Safe removal (destructive; confirm before executing) — full three-step sequence including the manual .git/modules/ cleanup: references/submodules.md.
  • Move an embedded .git into .git/modules/: rtk git submodule absorbgitdirs [<path>...] — needed when a submodule was created or copied without going through git submodule add. Details: references/submodules.md.

Configuration

.gitmodules (version-controlled, shared with collaborators):

Key Purpose
submodule.<name>.path Working tree path
submodule.<name>.url Remote URL
submodule.<name>.branch Branch used by update --remote
submodule.<name>.update Default update procedure
submodule.<name>.shallow Recommend shallow clone

.git/config (local only, populated by init):

Key Purpose
submodule.<name>.url Local URL override
submodule.<name>.update Local procedure override
submodule.fetchJobs Default parallelism for update --jobs
submodule.recurse Auto-recurse submodule updates on pull/push/etc.
rtk git config submodule.recurse true   # keep submodules pinned automatically after every pull

Agent output format

Return results as structured data:

operation: <clone|add|init|update|sync|set-url|set-branch|status|summary|absorbgitdirs|remove>
status: <success|error|partial>
message: <human-readable summary>
details:
  - <submodule-path>: <state>
conflicts: [<submodule-path>, ...]  # if any
next_step: <recovery action if applicable>

For errors, include the git command output and recommend recovery (e.g., git submodule deinit, force-update, or URL override).