Adding ht-gitignore-setup to hermes-talaria — Separating Scope Resolution, Template Pinning, and Pre-Write Verification
A .gitignore file looks like something you can produce by concatenating templates from github/gitignore, but in practice that path tends to pull already-tracked files into the ignore set, reorder ! negation lines so documented exceptions quietly stop working, and drop Global/ OS and editor templates into a shared project file. GitHub Docs’ Ignoring files does not call these failure modes out directly.
I added a skill that hands template selection, revision, and verification to an AI agent, in hermes-talaria. The PR is codenote-net/hermes-talaria#61.
The skill name is ht-gitignore-setup, invoked as /ht-gitignore-setup. Targets are the current directory, another local directory, a https://github.com/OWNER/REPOSITORY URL, or a https://github.com/OWNER/REPOSITORY/tree/REF/PATH tree URL. This post walks through the design decisions I made along the way.
Overview of What the Skill Adds
- A Hermes Agent skill that hands
.gitignorecreation and improvement to an AI agent - Accepts a local directory, a GitHub repository URL, or a GitHub tree URL. Issue, pull-request, blob, non-GitHub, and credential-embedding URLs are rejected
- For monorepo support, keeps repository context root, detailed scan target, rule scope, and
.gitignoreplacement path as four separate concepts - Resolves
github/gitignore’smainSHA once per run and fetches every selected template from raw URLs pinned to that one snapshot - Does not merge
Global/OS and editor templates into a shared project file by default. Lockfiles are not ignored by default either - Before writing, evaluates candidate rules in a temporary validation repository outside the real worktree, and detects tracked-path conflicts with
git check-ignore --no-index - For a newly created
.gitignore, usesgit diff --no-index --checkto run the whitespace gate, because plaingit diffhas nothing to show on an untracked file - Preserves existing rules, the order of negation (
!) lines, and intentional exceptions. Deletions require clear evidence - Does not run
git rm --cached,git init, commit, push, or read.env, key, or credential contents
Resolving Repository Context and Scope as Four Separate Things
The first design question was deciding “where to look, and where to write.” When a tree URL points at /apps/web in a monorepo, is the scan limited to that subtree, or should the whole repository be inspected? Does the rule scope stop at that subdirectory, or does it touch the root .gitignore?
SKILL.md calls out four distinct concepts and requires each to be stated before any change:
- repository context root
- detailed scan target
- rule scope
.gitignorepath
The defaults by input are:
| Input | Default scope | .gitignore placement | Detailed scan target |
|---|---|---|---|
| No target | repository | current worktree root | whole worktree |
| Explicit repository root | repository | worktree root | whole worktree |
| Repository URL | repository | clone root | whole clone |
| Nested local directory | target | requested subdirectory | requested subtree (root inspected for context) |
| GitHub tree URL | target | requested subdirectory | requested subtree (root inspected for context) |
| Non-Git directory | directory | requested directory | requested directory |
When existing monorepo policy conflicts with these defaults, the skill does not silently redirect the change; it obtains the user’s scope decision instead. An explicit user request for repository or target scope overrides the defaults.
For GitHub tree URLs, the REF/PATH split becomes ambiguous when the branch name contains /. Rather than guessing, the skill resolves REF through gh or the GitHub API first, then validates PATH. On clone, it checks out the resolved commit (normally as detached HEAD) instead of touching the default branch and works against that snapshot.
A URL containing userinfo (https://user:[email protected]/...) is rejected without being written anywhere. The policy is to ask for a clean HTTPS URL rather than sanitize-and-continue, so that credential values do not land in logs, ledgers, or shell argument lists.
Pinning github/gitignore Templates to a Single SHA
Provenance and reproducibility are typically treated casually in .gitignore work. Fetching from github/gitignore’s main repeatedly can hand you different template contents inside a single skill run, which breaks any recorded provenance for the generated file.
In template-sources.md, the primary source is restricted to github/gitignore (CC0-1.0), and retrieval uses two steps:
- At the start of the run, resolve
mainto one SHA - Fetch every selected template through a raw URL pinned to that SHA
UPSTREAM_SHA="$(curl -fsSL https://api.github.com/repos/github/gitignore/commits/main \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["sha"])')"
curl -fsSL \
"https://raw.githubusercontent.com/github/gitignore/${UPSTREAM_SHA}/Node.gitignore" \
-o "<TEMP_DIR>/Node.gitignore"Where python3 is unavailable, jq or gh api parses the JSON. Parsing JSON with regular expressions is not permitted. HTTP status checks and rejection of empty responses are also required.
Root-level templates (Node.gitignore, Python.gitignore, and so on) take precedence; community/ templates are allowed only when repository evidence supports a specialized technology. Global/ OS and editor templates are not merged into a shared project file by default. Writing macOS .DS_Store, JetBrains .idea/, or Vim *.swp rules into a shared .gitignore based on the current operator’s machine mixes a personal global-excludes concern into something all contributors share and tends to conflict with others’ environments.
Falling back to remembered template text when upstream retrieval fails is also forbidden. The skill either writes a narrow set of project-specific rules grounded in local evidence and discloses that no upstream template was used, or stops when a verified upstream baseline is required.
Inspect the Project Before Rewriting
Pasting a template and then “deleting the blocks you don’t need” tends to overwrite an existing .gitignore and pull tracked files into the ignore set. SKILL.md fixes the order: inspect the project first, then let only evidenced technologies drive template selection.
Information gathered:
- existing root and nested
.gitignorefiles .git/info/excludeandcore.excludesFile, consulted only to distinguish local behavior from repository policy- tracked paths (
git ls-files), untracked paths, and ignored paths (git status --short --untracked-files=all --ignored) - top-level and workspace-level manifests, lockfiles, build configuration, tool-version files, CI configuration
- a bounded-depth directory listing, excluding
.git - existing instructions such as
AGENTS.md,CLAUDE.md, and contribution docs
During this step, the skill does not read .env, key, or credential contents. It inspects filenames and documented naming conventions only. It also commits to “do not infer an ecosystem solely from an untracked generated directory”: a template becomes a candidate only when a manifest, configuration file, tracked source extension, or documentation corroborates the technology.
Validate Candidate Rules Outside the Worktree
The most common .gitignore accident is “add a rule that quietly includes files that are already tracked.” Ignore rules alone leave tracked files tracked, but a later git rm --cached turns that match into a history-level removal, and files referenced by CI or production start disappearing.
This skill never runs git rm --cached itself, but it does need to decide, before writing, whether a candidate rule would match any currently tracked path.
The important subtlety is that git check-ignore treats tracked paths as “not ignored” by default, so a candidate rule that logically matches a tracked path passes without a reported hit. SKILL.md names this behavior explicitly and requires --no-index for the check:
git -C "<REPO_ROOT>" ls-files -z \
| git -C "<REPO_ROOT>" check-ignore --stdin -z -v --no-indexThe check runs against the candidate rules, not the unchanged worktree. Rather than writing the candidate .gitignore into the real tree as a pre-check shortcut, the skill builds a temporary validation repository outside the worktree. That validation repository reproduces the candidate .gitignore’s repository-relative path, the relevant ancestor and descendant .gitignore files, and the original tracked-path list. Files whose contents are not needed are placeholders; files that might contain secrets (such as .env) are not copied by content.
Every matched tracked path is a review item. A tracked artifact can be intentional (a migration script, a test fixture), so being “newly included by an ignore rule” does not translate into automatic deletion or an automatic git rm --cached.
For every important rule or exception, representative paths are tested on both sides: one path that should be ignored, and one in the same directory that must remain visible. --non-matching (-n) is used in combination so the “did not match” result is explicit and the logic does not rely on command silence.
Verify a New .gitignore with --check, Not Plain Diff
When an existing file is modified, git diff --check plus git diff is enough to verify. A newly created .gitignore, however, is still untracked, so plain git diff shows nothing. Running the whitespace check without arguments produces no output either, which makes it easy to “created a file and called it verified.”
SKILL.md requires --no-index for newly created .gitignore files:
git diff --no-index -- /dev/null "<ABSOLUTE_GITIGNORE_PATH>"
git diff --no-index --check -- /dev/null "<ABSOLUTE_GITIGNORE_PATH>"The first command shows the content diff. --no-index diff returns exit 1 when the two inputs differ, which is not an error here, so the skill’s logic looks at the output for whitespace errors instead. The second command (--check) reports trailing whitespace, mixed indentation, and similar whitespace issues.
After writing, representative ignored and retained paths are checked with git check-ignore -v -n --no-index, the .gitignore itself is confirmed to be visible to Git, and manifests, lockfiles, source directories, fixtures, migrations, and sample configuration are confirmed to remain unignored. When a check fails, the rule is fixed and all affected checks are rerun; file existence alone does not promote the result to success.
Preserve Existing Rules and the Order of ! Negations
Modifying an existing .gitignore is a targeted patch rather than a wholesale replacement. The meaning of a negation line depends on the exclusion that precedes it, so reordering silently invalidates documented exceptions.
# Example: ignore everything under /dist/ except the committed samples
/dist/
!/dist/examples/Moving /dist/ after !/dist/examples/ strips the negation of its meaning. SKILL.md requires:
- when a negation depends on a preceding exclusion, preserve the order
- when an existing unusual rule’s purpose is unclear, retain it and flag it in the report rather than deleting it speculatively
- avoid introducing duplicate blocks, contradictory patterns, or speculative catch-all rules
The rewrite follows the principle “ignore reproducible or local-only artifacts; retain source, lockfiles, migrations, fixtures, examples, and intentionally committed configuration.”
- When only a root output directory needs ignoring, use a directory-anchored pattern such as
/dist/, notdist/everywhere - Ignore secret-bearing local files such as
.envconservatively, and add explicit sample exceptions such as!.env.exampleonly when those samples exist or repository policy requires them - Do not ignore all files of a source-like extension because some generated files share it
- Do not ignore lockfiles by default. Follow the ecosystem and the repository’s established policy
Define the Report Contents in SKILL.md
The skill’s output is constrained to a defined set of report items:
- the resolved local target, scan root, scope, and
.gitignorepath - whether the file was created or updated
- detected technologies and the evidence used to identify them
- exact upstream template paths, the pinned SHA, and the commit URL, or that no upstream template was used
- important project-specific rules and deliberate omissions
- tracked-file conflicts found during validation and how each was resolved
- commands and checks executed, and their real outcomes
- remaining uncertainty or follow-up that requires user intent
For runs against a GitHub URL, SKILL.md keeps “a change in the clone” and “a change in the remote repository” distinct. Rewriting files in a tree-URL clone does not change the repository on GitHub, so the report does not describe the GitHub repository as updated. Commit, push, and pull-request creation are opt-in on separate user intent.
Operations the Skill Does Not Perform
SKILL.md lists operations that are not performed even to finish faster:
- Removing files from history, purging secrets, or running
git clean - Removing tracked files with
git rm --cached - Running
git initto validate or initialize - Committing, pushing, forking, or creating pull requests
- Reading
.env, cookie, authentication-file, or key contents - Sanitizing credential-embedding URLs and continuing
- Writing signed URLs or credential values into logs or ledgers
- Substituting remembered template text, search-result snippets, or AI-generated broad patterns for an upstream template
- Writing macOS, Windows, Linux, editor, or personal-tool noise into a shared file solely because it exists on the current machine
- Deleting existing rules of unknown intent without evidence
- Auto-converting a “tracked artifact” to untracked
- Silently redirecting scope when the repository’s monorepo policy conflicts with the default
That’s all from adding ht-gitignore-setup to hermes-talaria and settling on a design that keeps scope resolution, upstream SHA pinning, and pre-write verification on separate axes, from the Gemba.