first commit
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
---
|
||||
name: go-web-app-no-deps
|
||||
description: Build a server-rendered web application in Go using ONLY the standard library (net/http, html/template, database/sql, embed) — no web framework (no Gin/Echo/Fiber/gorilla), no template engine beyond stdlib html/template, no third-party JS framework. Uses a Flask/Jinja2-style template inheritance pattern (a base.html layout with named blocks that page templates override) and a dark Tailwind CSS theme with a defined CSS custom-property palette. Use this whenever the user asks for a Go web app, admin panel, dashboard, file manager, webmail client, or any browser-based tool built in Go without third-party dependencies, or mentions wanting "Flask-like templates in Go" or a dark Tailwind theme for a Go backend. Also use when debugging a Go html/template project where blocks from different pages are bleeding into each other, or where static assets aren't updating in the browser after a change.
|
||||
---
|
||||
|
||||
# Go Web App, No Third-Party Dependencies
|
||||
|
||||
Two real projects converged on this exact pattern: a multi-account webmail
|
||||
client and a browser-based file explorer, both pure Go + stdlib
|
||||
`html/template` + vanilla JS + Tailwind CSS dark theme. The patterns below
|
||||
are what actually worked after hitting real bugs — not a theoretical
|
||||
"here's how you'd probably do it."
|
||||
|
||||
## Project layout
|
||||
|
||||
```
|
||||
project/
|
||||
├── cmd/appname/main.go
|
||||
├── internal/
|
||||
│ ├── handlers/ # HTTP handlers, one file per feature area
|
||||
│ ├── <domain logic>/ # the actual app logic, no http.* imports here
|
||||
│ └── render/ # the template Renderer (see below)
|
||||
├── web/
|
||||
│ ├── templates/
|
||||
│ │ ├── base.html # the ONE layout every page extends
|
||||
│ │ └── <page>.html # one file per page (login.html, app.html, admin.html...)
|
||||
│ └── static/
|
||||
│ ├── css/appname.css
|
||||
│ └── js/appname.js
|
||||
└── go.mod
|
||||
```
|
||||
|
||||
Embed `web/` into the binary with `//go:embed web/templates web/static` on
|
||||
a package-level `embed.FS` var — ships as one binary, no separate asset
|
||||
deploy step.
|
||||
|
||||
## The template renderer — get this right first, it's where the real bug lives
|
||||
|
||||
**The wrong way (looks fine until you have 2+ pages):** parsing every
|
||||
template file together with `template.ParseGlob("web/templates/*.html")`
|
||||
into one shared template set. Every page file typically defines a block
|
||||
named the same thing (`{{define "content"}}` or similar) to plug into the
|
||||
layout. When they're all parsed into one shared namespace, **the last
|
||||
one parsed silently wins for every page** — page A renders page B's
|
||||
content, or a block from one page bleeds into another. This is subtle:
|
||||
it can look correct for a single page and only breaks once a second page
|
||||
exists.
|
||||
|
||||
**The right way:** parse the base layout + exactly one page file into a
|
||||
**fresh template instance per page**, so no other page's `{{define}}`
|
||||
blocks are ever in the same namespace.
|
||||
|
||||
```go
|
||||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Renderer struct {
|
||||
templates map[string]*template.Template
|
||||
}
|
||||
|
||||
// pages lists every page template that extends base.html.
|
||||
var pages = []string{"login.html", "app.html", "admin.html"}
|
||||
|
||||
func New(fsys embed.FS) (*Renderer, error) {
|
||||
r := &Renderer{templates: make(map[string]*template.Template, len(pages))}
|
||||
for _, page := range pages {
|
||||
// Fresh template.New("base") EACH iteration — this is the whole fix.
|
||||
t, err := template.New("base").ParseFS(fsys, "web/templates/base.html", "web/templates/"+page)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing %s: %w", page, err)
|
||||
}
|
||||
name := page[:len(page)-len(".html")]
|
||||
r.templates[name] = t
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Render executes "base" (the layout), which pulls in this page's block
|
||||
// overrides — rendered to a buffer first so a mid-execution template error
|
||||
// never sends a half-written response to the client.
|
||||
func (r *Renderer) Render(w http.ResponseWriter, status int, name string, data any) {
|
||||
t, ok := r.templates[name]
|
||||
if !ok {
|
||||
http.Error(w, "unknown page: "+name, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := t.ExecuteTemplate(&buf, "base", data); err != nil {
|
||||
http.Error(w, "render error: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
buf.WriteTo(w)
|
||||
}
|
||||
```
|
||||
|
||||
Construct one `*Renderer` at startup (fails fast on any template syntax
|
||||
error) and pass it to every handler — don't reparse per-request.
|
||||
|
||||
## base.html — the Flask-`{% block %}` equivalent
|
||||
|
||||
Go's `html/template` has `{{block "name" .}}...default...{{end}}`
|
||||
(defines a block with a default body, overridable) and `{{define "name"}}`
|
||||
(overrides it from another file) — this is structurally the same idea as
|
||||
Jinja2's `{% block %}` / `{% extends %}` + child-template overrides, just
|
||||
different syntax.
|
||||
|
||||
```html
|
||||
{{define "base"}}<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{block "title" .}}App{{end}}</title>
|
||||
<link rel="stylesheet" href="/static/css/app.css?v={{.AssetVersion}}">
|
||||
{{block "head_extra" .}}{{end}}
|
||||
</head>
|
||||
<body class="{{block "body_class" .}}{{end}}">
|
||||
{{block "body" .}}{{end}}
|
||||
<script src="/static/js/app.js?v={{.AssetVersion}}"></script>
|
||||
{{block "scripts" .}}{{end}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
```
|
||||
|
||||
A page file overrides only the blocks it needs:
|
||||
|
||||
```html
|
||||
{{define "title"}}Sign in{{end}}
|
||||
{{define "body_class"}}login-page{{end}}
|
||||
{{define "body"}}
|
||||
<div class="login-card">
|
||||
<h1>Sign in</h1>
|
||||
<form id="login-form">...</form>
|
||||
</div>
|
||||
{{end}}
|
||||
```
|
||||
|
||||
`.AssetVersion` (a build timestamp or git short-hash passed into every
|
||||
`Render` call's data) matters more than it looks — see cache-busting below.
|
||||
|
||||
## Static assets: embed + version query param, always
|
||||
|
||||
```go
|
||||
staticFS, _ := fs.Sub(webFS, "web/static")
|
||||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
|
||||
```
|
||||
|
||||
Browsers aggressively cache `/static/*` files. During active development
|
||||
this reliably causes "I edited the CSS/JS and nothing changed" confusion.
|
||||
Fix it at the template level, not by fighting cache headers: put a
|
||||
version string in every static asset URL (`app.css?v=<version>`) and bump
|
||||
it whenever the file changes — a build timestamp baked in at compile time
|
||||
via `-ldflags "-X main.buildVersion=..."` works well and needs no manual
|
||||
bumping. For endpoints returning dynamic content that must never be
|
||||
stale even mid-session (an edited file's raw content, a just-changed
|
||||
API response), also set explicit headers: `Cache-Control: no-cache` for
|
||||
GETs that should always revalidate, `Cache-Control: no-store` for
|
||||
mutating POSTs.
|
||||
|
||||
## Dark Tailwind theme — CSS custom properties, not hardcoded hex everywhere
|
||||
|
||||
Use Tailwind's CDN build (`<script src="https://cdn.tailwindcss.com">`)
|
||||
for utility classes, but define the actual palette as CSS custom
|
||||
properties once, referenced from both Tailwind's arbitrary-value syntax
|
||||
and any hand-written CSS — changing the theme later means editing one
|
||||
`:root` block, not hunting hex codes through every template.
|
||||
|
||||
A palette that's worked well across real dark-themed Go apps:
|
||||
|
||||
```css
|
||||
:root{
|
||||
--bg:#0d0f14; --surface:#111318; --surface2:#161920; --surface3:#1c1f28;
|
||||
--border:#1e2330; --border2:#252a38;
|
||||
--text:#dde1ed; --text2:#9aa0b8; --muted:#5a6278;
|
||||
--accent:#5b8def; --accent-dim:rgba(91,141,239,.12); --accent-glow:rgba(91,141,239,.2);
|
||||
--danger:#ef4444; --success:#22c55e; --warn:#f59e0b;
|
||||
}
|
||||
html,body{height:100%;background:var(--bg);color:var(--text);font-family:system-ui,sans-serif}
|
||||
```
|
||||
|
||||
`--surface` / `--surface2` / `--surface3` give you three stacked-panel
|
||||
depths (page background → card → nested card) without inventing new
|
||||
grays each time. `--accent-dim` / `--accent-glow` are for hover/active
|
||||
states and focus rings — a translucent version of the accent color reads
|
||||
as "highlighted" without needing a second color.
|
||||
|
||||
Common components worth having a convention for from the start: a CSS
|
||||
spinner (border-based, no image/SVG dependency), a toast notification
|
||||
container (`position:fixed`, stacked, auto-dismiss), and a context-menu
|
||||
pattern (`position:fixed`, toggled via a `.open` class) — all three show
|
||||
up in nearly every admin/dashboard/file-manager style app.
|
||||
|
||||
## JavaScript: vanilla, but watch function scoping
|
||||
|
||||
No framework, no bundler — plain `<script>` tags, functions attached via
|
||||
`onclick="..."` or `addEventListener`. The one recurring bug: **don't
|
||||
declare a function inside a conditional block** if anything outside that
|
||||
block (including inline `onclick` handlers in the HTML) needs to call it:
|
||||
|
||||
```js
|
||||
// WRONG — startUpload is undefined outside this if-block's scope
|
||||
if (ALLOW_UPLOAD) {
|
||||
function startUpload() { ... }
|
||||
}
|
||||
|
||||
// RIGHT — always declared, gate the *behavior* inside instead
|
||||
function startUpload() {
|
||||
if (!ALLOW_UPLOAD) return;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Fetch calls follow one plain convention: JSON in, JSON out, `X-CSRF-Token`
|
||||
header on every mutating request (see below), and a shared `api()` helper
|
||||
that throws on non-2xx so callers can `try/catch` uniformly instead of
|
||||
checking `res.ok` everywhere by hand.
|
||||
|
||||
## Security basics worth building in from the start, not bolting on later
|
||||
|
||||
- **CSRF (double-submit cookie)**: generate a random token
|
||||
(`crypto/rand`), set it as a `SameSite=Strict` cookie, and require the
|
||||
same value echoed back in an `X-CSRF-Token` header on every mutating
|
||||
request; reject if they don't match. No session-store dependency needed.
|
||||
- **Path traversal**: for any handler that takes a path from the client
|
||||
(file browser, static content by name), resolve it with
|
||||
`filepath.EvalSymlinks` and verify the result still has the intended
|
||||
root directory as a prefix before touching the filesystem.
|
||||
- **Atomic writes**: write to a temp file in the same directory, then
|
||||
`os.Rename` over the target — never write in place, so a crash
|
||||
mid-write can't corrupt existing content.
|
||||
- **CSP header**: set a `Content-Security-Policy` on HTML responses even
|
||||
in a no-framework app — it's one header, not a dependency.
|
||||
|
||||
## What "no third-party dependencies" means in practice
|
||||
|
||||
Pure stdlib covers further than people expect for this kind of app:
|
||||
`net/http` (routing via `http.ServeMux` — Go 1.22+'s method+pattern
|
||||
matching like `mux.HandleFunc("POST /api/users", ...)` removes most of
|
||||
the reason people reach for a router library), `html/template`,
|
||||
`encoding/json`, `database/sql` (+ a driver — `mattn/go-sqlite3` or
|
||||
similar is the one common exception, since Go has no built-in SQL
|
||||
driver), `crypto/rand` for tokens, `embed` for shipping assets in the
|
||||
binary. Reach for a third-party package only when the standard library
|
||||
has no answer at all (a specific DB driver, CGO-only bindings) — not for
|
||||
convenience on things stdlib already does.
|
||||
Binary file not shown.
@@ -0,0 +1,199 @@
|
||||
---
|
||||
name: iterative-build-discipline
|
||||
description: Practices for building large, multi-phase software projects (servers, protocol implementations, hand-rolled clients, multi-session builds) where correctness and honest progress tracking matter more than speed. Use this whenever a project spans multiple sessions or phases, involves implementing a network protocol or parser from scratch, needs to prove correctness without access to the real external service it talks to, or risks losing state between sessions (sandbox resets, context limits, long-running builds). Also use when maintaining living planning/handover documents across many edits, or when deciding whether to test something before believing it works. Trigger this proactively for phased builds even if the user doesn't ask for "best practices" — the value is in not needing to be asked.
|
||||
---
|
||||
|
||||
# Iterative Build Discipline
|
||||
|
||||
Distilled from building GoMail, a ~14,000-line self-hosted email server
|
||||
implemented phase-by-phase across many sessions (SMTP/IMAP/JMAP/CalDAV/
|
||||
ManageSieve/OAuth2/ACME, all hand-rolled on stdlib). The patterns below
|
||||
caught real bugs and prevented real losses, repeatedly, across nearly
|
||||
every phase. This isn't theoretical advice — every rule here has a
|
||||
"here's the actual bug this caught" behind it.
|
||||
|
||||
## Core principle
|
||||
|
||||
**"It compiles" and "the happy path returned 200" are not evidence of
|
||||
correctness.** Every phase of GoMail had at least one real bug that a
|
||||
build-only check would have missed entirely and shipped silently. The
|
||||
single highest-leverage habit is: after writing a chunk of new
|
||||
functionality, write a real test that exercises it as a real client
|
||||
would, including the paths where it should fail. Do this before
|
||||
declaring the phase done, not as an afterthought.
|
||||
|
||||
## Testing philosophy
|
||||
|
||||
### Write disposable, real end-to-end tests — not unit-test theater
|
||||
For each meaningful chunk of work, write a throwaway test program that:
|
||||
1. Boots the real server(s) as goroutines/processes in the same test run
|
||||
2. Acts as a **real client** over the **real wire protocol** — raw TCP for
|
||||
custom protocols, real `net/http` for HTTP, not an in-process function
|
||||
call that skips the actual serialization/parsing
|
||||
3. Asserts against real downstream state (a database row, a file on disk),
|
||||
not just a status code
|
||||
4. Covers negative paths deliberately: wrong auth, malformed input,
|
||||
cross-user/cross-tenant access attempts, replay of a one-time token,
|
||||
unreachable dependency
|
||||
5. Gets deleted once it passes — it was scaffolding, not the deliverable
|
||||
|
||||
This pattern found a real bug in nearly every phase of a 15-phase project:
|
||||
a NULL-scan panic from a naively-wrapped nullable column, a boolean check
|
||||
that covered only one of two equivalent session kinds (silently dropping
|
||||
an entire code path for one server mode), a regex missing the `(?s)`
|
||||
DOTALL flag that made an entire class of multi-line responses parse as
|
||||
empty, a wire command sent with the wrong syntax that "worked" by
|
||||
returning an empty result instead of an error, a struct with no JSON tags
|
||||
that would have silently shipped `TotalCount` instead of `total_count` on
|
||||
the wire, a SQLite connection-pool deadlock from calling `Query` then
|
||||
`Exec` in the same function without closing `rows` first, and a fake
|
||||
protocol test server whose oversized buffered `Read()` silently over-read
|
||||
into subsequent protocol bytes and desynced its own parser (looked
|
||||
exactly like a real product bug until traced).
|
||||
|
||||
**None of these would have been caught by "the code compiles and looks
|
||||
right."** All of them were caught by writing a client that actually spoke
|
||||
the protocol and checking what actually happened downstream.
|
||||
|
||||
### When you can't test against the real external service, build a fake
|
||||
one that actually enforces the protocol — not a rubber stamp
|
||||
For OAuth2, ACME, ClamAV, rspamd, and an LLM server, there was no real
|
||||
instance available in the build environment (no real Google/Microsoft app
|
||||
credentials, no public domain to satisfy Let's Encrypt's HTTP-01
|
||||
validator, no ClamAV/rspamd/llama.cpp installed). The fix was never to
|
||||
skip testing — it was to build a **from-scratch fake server that
|
||||
genuinely implements the documented protocol**, then prove the real
|
||||
client code against it:
|
||||
- A fake ACME server that does real ES256 JWS signature verification and
|
||||
real RFC 7638 JWK thumbprint recomputation, and only issues a
|
||||
certificate if a real HTTP callback to the challenge responder actually
|
||||
succeeds — not a server that just returns "OK" to whatever it's sent.
|
||||
- A fake ClamAV that reassembles the streamed chunks and inspects the
|
||||
bytes for the real EICAR test signature, rather than returning a canned
|
||||
"clean" response regardless of input.
|
||||
- A fake OAuth2 authorization server implementing the real
|
||||
authorization-code + refresh-token grant flows, including one-time-use
|
||||
codes and CSRF state validation.
|
||||
|
||||
This is real verification of protocol correctness — not a substitute for
|
||||
eventually testing against the real service, but a much stronger check
|
||||
than "it compiles" and often catches the exact class of bug that would
|
||||
otherwise only surface against production. **Explicitly flag, in both
|
||||
code comments and any handoff docs, that this is fake-server verification
|
||||
and that live verification against the real service is still an open
|
||||
item** — don't let a passing fake-server test quietly become "this is
|
||||
proven to work."
|
||||
|
||||
### When a real published test vector exists, use it
|
||||
For anything implementing a spec with official test vectors (RFC 6238's
|
||||
TOTP Appendix B, for instance), verify against the actual published
|
||||
numbers rather than only checking internal self-consistency. This is a
|
||||
strictly stronger correctness check: internal self-consistency can pass
|
||||
while the implementation is confidently wrong in a way that happens to be
|
||||
symmetric; matching an external published vector rules that out.
|
||||
|
||||
### A negative test is not optional polish
|
||||
Auth-failure paths, permission-boundary checks, tampered-input rejection,
|
||||
and replay-prevention are not "nice to have if there's time" — in this
|
||||
project, testing them found real security-relevant gaps (a privilege
|
||||
escalation attempt that wasn't actually blocked until tested, a CSRF
|
||||
state that could be replayed until a check was added, a signature
|
||||
verification path that would have accepted a forged input). Budget time
|
||||
for these as part of "done," not after it.
|
||||
|
||||
## Session and state continuity
|
||||
|
||||
### Checkpoint working state as an artifact, not just as conversation history
|
||||
In any environment where the working directory or container can reset
|
||||
(sandbox limits, context window limits, session boundaries), the actual
|
||||
recovery mechanism is a **packaged artifact saved outside the working
|
||||
directory** — a tarball, a committed file, whatever the environment's
|
||||
durable output channel is. Do this after every meaningful increment, not
|
||||
just at the end. Conversation history describing what you did is not a
|
||||
substitute for the file existing somewhere durable.
|
||||
|
||||
### Before editing a living document, verify its current state — don't
|
||||
assume your last edit landed
|
||||
Long sessions with many sequential edits to the same planning/handover
|
||||
document are exactly where partial failures compound silently: an
|
||||
aborted tool call, a `str_replace` that matched the wrong location, or an
|
||||
edit applied against stale context can leave a document with duplicated
|
||||
or contradictory sections (a "done" entry sitting right next to a stale
|
||||
"not started" stub for the same item). Before trusting that a previous
|
||||
edit succeeded, check: grep for the section header count (should be
|
||||
exactly 1), read the actual current content of the section you're about
|
||||
to touch, and only then edit. After editing, verify again — don't chain
|
||||
edits on documents based on what you intended to have written rather than
|
||||
what's actually there.
|
||||
|
||||
### Keep a living plan document AND a separate onboarding/handover document
|
||||
The plan document is the source of truth for **what's done, what's
|
||||
tested, what's deferred and why** — organized by phase/milestone, updated
|
||||
every time scope changes. The handover document is written for **a fresh
|
||||
reader with zero context** — environment setup that differs between the
|
||||
build environment and a real deployment target, decisions not to
|
||||
re-litigate, and a prioritized "what to do next" list. These serve
|
||||
different readers and get stale in different ways; don't collapse them
|
||||
into one document, but do keep them in sync with each other (a completed
|
||||
phase should be reflected in both).
|
||||
|
||||
## Scoping and honesty
|
||||
|
||||
### When a sub-feature is genuinely out of scope for the current pass, say so explicitly — don't half-build it
|
||||
Real examples from this project: WebAuthn/passkeys were skipped entirely
|
||||
because they require CBOR decoding, which isn't in Go's standard library
|
||||
and is a large sub-project in its own right — attempting a partial
|
||||
implementation would have produced something that looked done but wasn't
|
||||
trustworthy. QR code rendering for TOTP setup was skipped in favor of the
|
||||
plain-text provisioning URI (which every real authenticator app accepts
|
||||
as manual entry) because real QR encoding is a separate, non-trivial
|
||||
piece of work. In both cases, the deferral was written down explicitly —
|
||||
in code comments and in the plan document — with the reason, rather than
|
||||
silently leaving a stub or, worse, a fake implementation that doesn't
|
||||
actually work.
|
||||
|
||||
### Prioritize by what actually reduces risk, not strictly by original task order
|
||||
When a later-numbered piece of work would close a security gap that
|
||||
affects everything already built (in this project: real TLS certificate
|
||||
issuance, since every existing self-signed-cert fallback was a flagged
|
||||
weakness), it's worth pulling that forward ahead of strict sequential
|
||||
order — and saying so explicitly, with the reasoning, rather than
|
||||
silently reordering.
|
||||
|
||||
### A fact stated in a handoff document should be checked, not assumed durable
|
||||
Version numbers, "as of" dates, and "extract this tarball" instructions
|
||||
written early in a long session go stale as the session continues. Before
|
||||
finalizing a handoff document, re-read it as if you were the person
|
||||
receiving it fresh — do the setup instructions still point at the right
|
||||
artifact? Does the "current phase" claim match what's actually true now?
|
||||
|
||||
## Technical patterns worth remembering generically
|
||||
|
||||
- **Raw TCP protocol work (client or test server)**: never assume one
|
||||
`Read()` call aligns with the sender's `Write()` call boundaries — TCP
|
||||
doesn't preserve them. Use a `bufio.Reader` with exact-length
|
||||
`io.ReadFull` reads for anything with a defined wire format (a command
|
||||
prefix, a length-prefixed chunk), not a single oversized buffered
|
||||
`Read()`.
|
||||
- **A database connection pool capped to one connection** (common for
|
||||
embedded databases like SQLite in a single-process server): never call
|
||||
a second query/exec while an earlier `*sql.Rows` from the same
|
||||
connection is still open — close it explicitly first, don't rely on a
|
||||
deferred close if you need the connection again before the function
|
||||
returns.
|
||||
- **Wire-format structs**: always give them explicit serialization tags.
|
||||
A struct with no tags will round-trip fine in same-language tests
|
||||
(Go-to-Go) while silently breaking the actual wire contract expected by
|
||||
any other client.
|
||||
- **When verifying a hand-rolled crypto/signing implementation**, prefer
|
||||
checking against a real published test vector over only checking
|
||||
internal round-trip consistency, wherever the spec provides one.
|
||||
|
||||
## What this skill is not
|
||||
|
||||
This isn't a checklist to run through mechanically per line — it's a set
|
||||
of habits earned from what actually went wrong across a large real
|
||||
project. The common thread: **verify against reality (real protocol,
|
||||
real published vectors, real negative paths, real current document
|
||||
state) instead of trusting that code which compiles or looks right is
|
||||
therefore correct.**
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash|Grep",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "/home/haku/.local/bin/graphify hook-guard search"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Read|Glob",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "/home/haku/.local/bin/graphify hook-guard read"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(go version *)",
|
||||
"Bash(command -v staticcheck)",
|
||||
"Bash(go build *)",
|
||||
"Bash(go vet *)",
|
||||
"Bash(go test *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user