Skip to content
349 changes: 349 additions & 0 deletions hips/hip-9999.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
---
hip: 9999
title: "Bring .helmignore to parity with .gitignore file targeting syntax"
authors: ["Scott Rigby <scott@r6by.com>"]
created: "2025-12-14"
type: "feature"
status: "draft"
requires: ["HIP-0020"]
---

## Abstract

This proposal brings `.helmignore` file targeting semantics to full parity with `.gitignore` syntax and matching rules for Helm Charts v3. The current `.helmignore` implementation diverges from `.gitignore` in critical ways—most notably in rule evaluation order (first-match vs. last-match), negation pattern behavior, and lack of `**` recursive glob support. These differences cause confusion and bugs for users who reasonably expect `.helmignore` to behave like `.gitignore`. By scoping this change to Charts v3 (per [HIP-0020][hip-0020]), existing charts continue to work unchanged while v3 charts opt into consistent, predictable ignore behavior.

## Motivation

Users familiar with `.gitignore` expect the same behavior from `.helmignore`. The current divergence causes:

1. **Broken negation patterns**: Users cannot use whitelist-style patterns (e.g., `/*` then `!Chart.yaml`) because the current implementation has inverted negation semantics ([#8688], [#3622], [#1776]).

2. **Missing recursive globs**: The `**` pattern is explicitly unsupported, forcing verbose workarounds for common cases like `**/test/` ([#12592]).

3. **Unexpected first-match behavior**: Git uses "last matching rule wins"; Helm stops at the first match. This breaks patterns that progressively refine what to exclude.

4. **Documentation gaps**: Current docs don't clearly explain limitations or differences from `.gitignore` ([#4638], [helm-www#1312]).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is one more problem iirc -- today Helm only respects .helmignore for helm package. It will ignore .helmignore if the chart is unpacked/untarred

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.helmignore definitely ignores files during helm runtime operations too - on both a packed ann unpacked chart - including helm template, install, etc. Here is an unpacked example:

d=$(mktemp -d)
mkdir -p $d/templates $d/files

cat > $d/Chart.yaml <<'EOF'
apiVersion: v2
name: test
version: 0.1.0
EOF

cat > $d/templates/cm.yaml <<'EOF'
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Chart.Name }}-cm
data:
{{- (.Files.Glob "files/*").AsConfig | nindent 2 }}
EOF

cat > $d/templates/pod.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: {{ .Chart.Name }}-pod
spec:
  containers:
  - name: nginx
    image: nginx:alpine
EOF

cat > $d/files/test.conf <<'EOF'
test including .conf file
EOF

cat > $d/files/test.txt <<'EOF'
test including .txt file
EOF

tree $d

echo '\n=== template WITHOUT .helmignore ==='
helm template $d

cat > $d/.helmignore <<'EOF'
*.txt
pod.yaml
EOF

echo '\n=== template WITH .helmignore ==='
helm template $d

echo '\n=== install WITH .helmignore ==='
helm install --dry-run test $d

should return

/var/folders/7j/bxcg16h177zfgfkzdlv8rb240000gn/T/tmp.5QdzSnmxHz
├── Chart.yaml
├── files
│   ├── test.conf
│   └── test.txt
└── templates
    ├── cm.yaml
    └── pod.yaml

3 directories, 5 files

=== template WITHOUT .helmignore ===
---
# Source: test/templates/cm.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: test-cm
data:
  test.conf: |
    test including .conf file
  test.txt: |
    test including .txt file
---
# Source: test/templates/pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-pod
spec:
  containers:
  - name: nginx
    image: nginx:alpine

=== template WITH .helmignore ===
---
# Source: test/templates/cm.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: test-cm
data:
  test.conf: |
    test including .conf file

=== install WITH .helmignore ===
level=WARN msg="--dry-run is deprecated and should be replaced with '--dry-run=client'"
NAME: test
LAST DEPLOYED: Wed Dec 17 22:14:13 2025
NAMESPACE: default
STATUS: pending-install
REVISION: 1
DESCRIPTION: Dry run complete
TEST SUITE: None
HOOKS:
MANIFEST:
---
# Source: test/templates/cm.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: test-cm
data:
  test.conf: |
    test including .conf file
  • Without .helmignore, both files are globbed into the configmap, and the pod template is rendered.
  • With .helmignore containing *.txt and pod.yaml only the non .txt file is globbed into the configmap, and the pod is not rendered.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, digging up my reference where I think we deemed .helmignore was not being used.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, I was mistaken: helm/helm#12592 (comment)

User was reporting .helmignore was not being respected during helm package. But this just seems like an outright bug.

### Evidence of User Pain

A comprehensive search of helm/helm issues reveals **27 directly related issues** and **5 pull requests**. Key themes:

- **Pattern matching logic**: [#8688], [#3622], [#1776], [#12592]
- **Scope/behavior confusion**: [#6075], [#3050], [#10764]
- **Documentation issues**: [#4638], [helm-www#1171], [helm-www#1312], [helm-www#1460]

The recurring user expectation is clear: `.helmignore` should work like `.gitignore`.

## Rationale

### Why .gitignore Parity?

1. **Reduced cognitive load**: Developers already know `.gitignore` semantics. Aligning `.helmignore` enables existing knowledge and patterns to be reused with Helm.

2. **Well-documented spec**: Git's ignore format is [thoroughly documented][git-gitignore] and battle-tested across millions of repositories.

3. **Ecosystem consistency**: Tools like Docker (`.dockerignore`), npm (`.npmignore`), and FluxCD (`.sourceignore`) all follow `.gitignore` semantics.

### Why Charts v3?

Per [HIP-0020][hip-0020], Charts v3 provides a clean opt-in boundary for breaking changes:

1. **Breaking change isolation**: Charts explicitly declare `apiVersion: v3`, accepting new semantics. Charts v2 behavior is preserved unchanged.

2. **No forced migration**: Existing charts continue to work. Users migrate on their own schedule.

3. **Independent evolution**: Chart changes are decoupled from Helm version, allowing adequate time for testing and adoption.

This is the appropriate scope because changing `.helmignore` semantics would break charts that depend on current (albeit surprising) behavior.

## Specification

### Behavioral Parity with .gitignore

Charts v3 `.helmignore` files should support the full `.gitignore` pattern format as documented at [git-scm.com/docs/gitignore][git-gitignore].

#### Design Intent

`.helmignore` is intended to follow Git's `.gitignore` file pattern matching semantics as documented at the time of this HIP's acceptance. These semantics have been intentionally stable for many years, and familiarity with them is a primary goal of this proposal.

If Helm's `.helmignore` behavior diverges from Git's documented `.gitignore` behavior, that divergence should be treated as a bug and corrected—unless the divergence is explicitly documented in this proposal (see Scope Clarification).

**Future Git changes**: If Git were to introduce incompatible changes to `.gitignore` matching semantics in the future, Helm would evaluate and explicitly decide whether to adopt those changes. Helm does not implicitly inherit future Git behavior changes.

#### Recursive Glob Semantics (`**`)

- `**/foo` — matches `foo` in all directories
- `foo/**` — matches everything inside `foo/`
- `a/**/b` — matches `a/b`, `a/x/b`, `a/x/y/b`, etc.

#### Rule Evaluation Order

**Last matching rule wins**. All rules are evaluated; the final matching rule determines whether a path is included or excluded. This enables patterns like:

```
# Exclude all top-level entries (files and directories)
/*

# But include these
!Chart.yaml
!values.yaml
!templates/
```

#### Negation Behavior

- `!pattern` re-includes files that match, reversing a prior exclusion
- **Limitation**: Cannot re-include a file if its parent directory was excluded (Git skips listing excluded directories for performance)

#### Scope Clarification

Helm has no concept of staging or committing files. This proposal addresses **file targeting syntax and semantics only**—specifically, which files are included/excluded during chart operations. Git's behavior around staged/committed files does not apply.

#### File Location

Charts v3 `.helmignore` is loaded from the chart root directory only. Unlike `.gitignore`, which supports recursive ignore files in subdirectories within a repository, Helm loads `.helmignore` only from the chart root. Patterns in the root `.helmignore` apply to the entire chart tree.

Recursive `.helmignore` loading within a chart (matching Git's nested `.gitignore` behavior) is a potential future enhancement but is out of scope for this HIP.

### Technical Requirements
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to specify that we (presumably) will only support a single .helmignore file (in the root of the chart). Or, we will match git's recusive .gitignore behavior:

Patterns read from a .gitignore file in the same directory as the path, or in any parent directory (up to the top-level of the working tree), with patterns in the higher level files being overridden by those in lower level files down to the directory containing the file. These patterns match relative to the location of the .gitignore file.

(and iirc, there is text which specifys/assumes .helmignore is in the root of the chart filesystem layout)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call-out. Added a "File Location" subsection clarifying that .helmignore loads only from the chart root (existing intended behavior).

For subchart .helmignore files specifically, added this as an Open Issue - unclear if the current behavior (ignoring them) is intended, a bug, or an oversight. I'll investigate and raise at the next dev meeting. For now, this HIP preserves existing behavior.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also perhaps include the equivalent of:

Git does not follow symbolic links when accessing a .gitignore file in the working tree.


Git has a specific reasons to not follow:

Git does not follow symbolic links when accessing a .gitignore file in the working tree. This keeps behavior consistent when the file is accessed from the index or a tree versus from the filesystem.

This rationale does not exactly apply to Helm I think. If we want to be consistent with .gitignore, we would want to apply this to Helm.

Otherwise, this perhaps the same question as to what helm package should do with respect to external symlinks.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Git's symlink note is specific to Git's consistency between index/tree and filesystem access - Helm has no equivalent concept. This HIP addresses file targeting syntax only; Helm's existing symlink handling (following symlinks before applying ignore rules) is unchanged and out of scope.

PS - separate from this HIP - I do think we should better document Helm's symlink behavior. From looking at the code, and testing out to make sure - here's what I see as helm package symlink behavior (current, unchanged by this HIP):

  1. Symlinked .helmignore: Helm follows the symlink to read the ignore rules - works as if the file were in-place
  2. Symlinks in chart content: Helm follows symlinks and expands them into the packaged tarball (the symlink becomes a regular file containing the target's content, not a symlink reference)
  3. Interaction with .helmignore:
    • Ignoring a symlink: excludes the symlink itself from the package
    • Ignoring a symlink's target: the symlink still resolves and becomes a file in the package (symlink resolution happens before ignore rules apply)
  4. Security restriction: External symlinks to certain sensitive paths are blocked per https://helm.sh/blog/2019/10/30/helm-symlink-security-notice/


1. **Parser/Matcher**: Implement a `.gitignore`-compatible lexer, parser, and matcher supporting all pattern types above.

2. **Last-match semantics**: Evaluate all rules; final matching rule determines outcome.

3. **Path normalization**: Normalize paths to forward slashes for cross-platform consistency.

4. **Directory short-circuit**: Once a directory is excluded, skip traversing its contents (matches Git's performance optimization).

### Integration Points

The new `.gitignore`-parity matching should apply in all existing chart file operations (unchanged scope):

- `helm package`
- `helm lint`
- `helm template`
- `helm install` / `helm upgrade` (for local charts)
- `.Files.Get` / `.Files.Glob` (file access in templates)

Comment on lines +126 to +127
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- `.Files.Get` / `.Files.Glob` (file access in templates)
- `.Files.Get` / `.Files.Glob` (file access in templates)
- etc

nitpick: This list might not be exhaustive

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - added: "This list is representative; the matching logic applies wherever chart files are loaded or accessed."

This list is representative; the matching logic applies wherever chart files are loaded or accessed.

### Gating

- **Charts v3 (`apiVersion: v3`)**: New `.gitignore`-parity semantics
- **Charts v2 (`apiVersion: v2`)**: Existing behavior unchanged

## Backwards Compatibility

### Charts v2

No behavior change. Existing `.helmignore` files continue to work exactly as before, preserving compatibility for all current charts.

### Charts v3

Charts opting into v3 accept new `.helmignore` semantics. Potential breaking changes for charts migrating from v2:

| Current Behavior | New Behavior | Migration Impact |
| ------------------------ | -------------------------- | ------------------------------------------------------------ |
| First-match evaluation | Last-match evaluation | Patterns relying on early termination may behave differently |
| Negation inverts match | Negation re-includes | Whitelist patterns will finally work correctly |
| `**` causes error | `**` works | No breakage (feature addition) |
| Trailing spaces stripped | Preserved with `\ ` | Unlikely to affect real charts |
| No escape sequences | `\#`, `\!`, `\ ` supported | No breakage (feature addition) |

## Security Implications

None. This change only affects which local files are considered during chart operations. It does not:

- Introduce new attack surfaces
- Add remote dependencies
- Change network behavior
- Affect chart signing or verification

## How to Teach This

### Documentation Updates

1. **Primary statement**: "Charts v3 `.helmignore` uses identical pattern rules to `.gitignore`."

2. **Link to Git documentation**: Reference [git-scm.com/docs/gitignore][git-gitignore] as background documentation for the pattern semantics that `.helmignore` follows.

3. **Migration guide**: Document behavior differences between v2 and v3.

4. **Examples**: Provide side-by-side comparisons showing:
- Whitelist patterns (`/*`, `!Chart.yaml`)
- Recursive globs (`**/test/`)
- Last-match ordering

5. **Improve v2 and v3 documentation**: Existing `.helmignore` documentation is insufficient, leading to end user confusion, so this should be documented as well.

6. **File location**: Clarify that `.helmignore` must be in the chart root directory. Unlike `.gitignore`, nested `.helmignore` files are not loaded. This existing behavior is unchanged.

### Scaffold Update

Update the default `.helmignore` generated by `helm create` for Charts v3. The current scaffold has been largely unchanged since 2016 ([helm#1028][#1028]). The v3 scaffold should demonstrate gitignore-parity patterns (e.g., `**` globs, whitelist patterns with `!`).

### Release Notes

- Highlight UX improvement for users expecting `.gitignore` behavior
- Link to this HIP and HIP-0020 for context
- Emphasize opt-in nature via Charts v3

### Example Patterns

```gitignore
# Comments start with #

# Ignore all dotfiles
.*

# But keep .helmignore itself
!.helmignore

# Ignore test directories anywhere
**/test/
**/tests/

# Ignore IDE files
.idea/
.vscode/
*.swp
*.swo
*~

# Whitelist approach: exclude everything, then include specifics
/*
!Chart.yaml
!values.yaml
!values.schema.json
!templates/
!charts/
!crds/
!files/

# Directory-only patterns (trailing slash)
vendor/
node_modules/

# Anchored to root (leading slash)
/local-only.yaml
```

## Reference Implementation

A proof-of-concept is available at [github.com/scottrigby/helmignore-ref][helmignore-ref], demonstrating that `go-git/go-git/v5/plumbing/format/gitignore` meets all HIP requirements with minimal wrapper code (~76 lines). The repository includes research documentation comparing library options.

Implementation will be gated behind Charts v3 as specified in [HIP-0020][hip-0020]:

1. New matcher package under `internal/chart/v3/ignore/` using go-git's gitignore library
2. Integration with v3 chart loader
3. Test suite covering all pattern types from this specification
4. Migration tests validating v2 charts remain unchanged
5. Update `helm create` scaffold for v3 charts (once [PR #31592][pr-31592] merges)

## Rejected Ideas

### Shell out to `git check-ignore`

Calling the `git` binary would provide perfect behavioral parity. Rejected because:

- Helm binaries must not have external runtime dependencies
- Adds complexity for containerized/minimal environments
- Performance overhead for repeated invocations

### Partial parity (e.g., add `**` but keep first-match)

Rejected because partial fixes would leave confusing inconsistencies. Users expect `.gitignore` behavior; half-measures perpetuate the problem.

### Change behavior for all chart versions

Rejected because it would break existing charts that depend on current (albeit surprising) semantics. Charts v3 provides clean isolation for breaking changes.

## Open Issues

### Subchart .helmignore Handling

Should `.helmignore` files in subchart directories (`charts/*/`) be respected when processing an umbrella chart?

**Current behavior**: Only the parent chart's `.helmignore` is loaded; subchart `.helmignore` files are ignored.

**Open question**: Is this intended behavior, a bug, or an oversight? Arguments exist both ways:

- **For respecting**: Chart authors expect their ignore rules to apply
- **Against (or: may not matter)**: Parent chart may want control; also, helm-ignored files would already be excluded when the subchart was packaged, so they likely won't be present anyway

**Action**: Investigate history (git commits, meeting notes, previous maintainers) and discuss with community before deciding whether to change this behavior. For now, this HIP preserves existing behavior.

## References

### Helm HIPs

- [HIP-0020: Charts v3 Enablement][hip-0020]

### Git Documentation

- [git-scm.com/docs/gitignore][git-gitignore]

### Libraries

- [go-git/go-git gitignore][go-git-gitignore] — Recommended implementation library

### Issues Directly Addressed by This HIP

These issues are resolved by implementing `.gitignore` pattern matching parity:

- [#8688][#8688] — Negation semantics are inverted; `!pattern` ignores non-matches instead of re-including matches.
- [#3622][#3622] — Whitelist patterns (`/*` then `!Chart.yaml`) fail with "chart metadata missing" due to broken negation logic.
- [#1776][#1776] — Pattern `.*` incorrectly matched the current directory, breaking charts. Shows user expectation of gitignore behavior.
- [#12592][#12592] — Patterns like `charts/*/README.md` don't work; `**` glob support and improved matching would help.
- [#12265][#12265] (PR) — Attempted partial fix for negation, stalled 2 years as a breaking change. This HIP provides the proper scope via Charts v3.

### Issues Out of Scope (Context Only)

These issues concern _when_ `.helmignore` applies, not pattern syntax. Included for context but not addressed by this HIP:

- [#6075][#6075] — Users expected ignore to affect only `helm package`, but it affects all commands. This is intentional; HIP clarifies but doesn't change this.
- [#3050][#3050] — Files in `.helmignore` are inaccessible to `.Files.Get`. Architectural issue about ignore scope, not pattern matching.
- [#10764][#10764] — README files consume release storage; users want "exclude from release but include in package." Requires new scope distinction.
- [#9436][#9436] — `.helmignore` doesn't exclude itself from packages. This is intentional becuase `.helmignore` is used for more than packaging, so various workflows could be disrupted (see https://github.com/helm/helm/issues/9436#issuecomment-792795063). Documentation update via this HIP should clarify this.

### Feature Requests (Out of Scope)

- [#1674][#1674], [#5675][#5675] — Global `~/.helmignore` support. Already partially implemented; could adopt gitignore parity separately.

### Documentation Issues

These show user confusion that better docs (and gitignore parity) would reduce:

- [#4638][#4638] — Requested more `.helmignore` documentation; shows need for clearer syntax docs.
- [helm-www#1171][helm-www#1171] — Docs say ignore affects "packaging" only, but it affects all operations. Needs correction regardless of HIP.
- [helm-www#1312][helm-www#1312] — Docs don't specify `.helmignore` must be in chart root, not working directory.
- [helm-www#1460][helm-www#1460] — Example pattern `/temp*` broke charts by matching `templates/`. Shows need for better examples.

### Other Related PRs

- [#13293][#13293] — Fix for broken symlinks in `.helmignore`. Tangentially related to ignore handling.

<!-- Link definitions -->

[hip-0020]: https://github.com/helm/community/blob/main/hips/hip-0020.md
[helmignore-ref]: https://github.com/scottrigby/helmignore-ref
[pr-31592]: https://github.com/helm/helm/pull/31592
[git-gitignore]: https://git-scm.com/docs/gitignore
[go-git-gitignore]: https://github.com/go-git/go-git/tree/main/plumbing/format/gitignore
[#8688]: https://github.com/helm/helm/issues/8688
[#3622]: https://github.com/helm/helm/issues/3622
[#1776]: https://github.com/helm/helm/issues/1776
[#12592]: https://github.com/helm/helm/issues/12592
[#6075]: https://github.com/helm/helm/issues/6075
[#3050]: https://github.com/helm/helm/issues/3050
[#9436]: https://github.com/helm/helm/issues/9436
[#10764]: https://github.com/helm/helm/issues/10764
[#1674]: https://github.com/helm/helm/issues/1674
[#5675]: https://github.com/helm/helm/issues/5675
[#1028]: https://github.com/helm/helm/pull/1028
[#4638]: https://github.com/helm/helm/issues/4638
[#12265]: https://github.com/helm/helm/pull/12265
[helm-www#1171]: https://github.com/helm/helm-www/issues/1171
[helm-www#1312]: https://github.com/helm/helm-www/issues/1312
[helm-www#1460]: https://github.com/helm/helm-www/issues/1460
[#13293]: https://github.com/helm/helm/pull/13293