Browse Source

agent guardrails: tclsh has no -e one-liner flag - AGENTS.md pitfall entry + claude PreToolUse deny hook

Stock tclsh treats any leading-dash arg other than -encoding as argv for an
interactive/stdin session: on a console it hangs at a prompt, with piped stdin
it exits 0 silently ignoring the supposed one-liner. Agents habitually reach
for -e (perl/python/node reflex), wasting time waiting on hung processes.

- root AGENTS.md User Preferences: document the misparse, the correct patterns
  (temp .tcl file or script via stdin) and the defensive stdin-redirect habit
- .claude/settings.json: PreToolUse hook denying Bash/PowerShell invocations
  of tclsh with -e, anchored to command position (start/pipe/semicolon/paren)
  so prose mentions in commit messages or grep args do not false-positive;
  corrective guidance returned to the agent
- .gitignore: narrow the .claude ignore so shared settings.json is tracked
  while session-local files stay ignored
- .fossil-settings: ignore-glob hand-derived per the no-negation rule
  (.claude/* wholesale + explicit fossil add of settings.json), exception set
  in AGENTS.md updated; fossil extras under-ignore check verified empty

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 2 days ago
parent
commit
06db38be46
  1. 50
      .claude/settings.json
  2. 2
      .fossil-settings/AGENTS.md
  3. 3
      .fossil-settings/ignore-glob
  4. 4
      .gitignore
  5. 1
      AGENTS.md

50
.claude/settings.json

@ -0,0 +1,50 @@
{
"permissions": {
"allow": [
"Bash(tclsh90 src/tests/runtests.tcl *)",
"Bash(tclsh src/tests/runtests.tcl *)",
"Bash(/c/Users/sleek/AppData/Local/Apps/Tcl903/bin/tclsh90 src/tests/runtests.tcl *)",
"Bash(\"C:/Users/sleek/AppData/Local/Apps/Tcl903/bin/tclsh.exe\" src/tests/runtests.tcl *)",
"Bash(./bin/punk91.exe src/tests/runtests.tcl *)",
"Bash(tclsh src/make.tcl *)",
"Bash(tclsh90 src/make.tcl *)",
"Bash(tclsh87 src/make.tcl *)",
"Bash(/c/Users/sleek/AppData/Local/Apps/Tcl903/bin/tclsh90 src/make.tcl *)",
"Bash(\"C:/Users/sleek/AppData/Local/Apps/Tcl903/bin/tclsh.exe\" src/make.tcl *)",
"Bash(./bin/punk91.exe src/make.tcl *)",
"Bash(./bin/punk902z.exe src/make.tcl *)",
"Bash(./bin/punksys.exe src/make.tcl *)",
"Bash(tclsh scriptlib/developer/goals_lint.tcl *)",
"Bash(tclsh90 scriptlib/developer/goals_lint.tcl *)",
"Bash(/c/Users/sleek/AppData/Local/Apps/Tcl903/bin/tclsh90 scriptlib/developer/goals_lint.tcl *)",
"Bash(\"C:/Users/sleek/AppData/Local/Apps/Tcl903/bin/tclsh.exe\" scriptlib/developer/goals_lint.tcl *)",
"PowerShell(tclsh scriptlib/developer/goals_lint.tcl *)",
"PowerShell(tclsh90 scriptlib/developer/goals_lint.tcl *)",
"PowerShell(& \"C:\\Users\\sleek\\AppData\\Local\\Apps\\Tcl903\\bin\\tclsh90.exe\" scriptlib/developer/goals_lint.tcl *)",
"PowerShell(tclsh src/make.tcl *)",
"PowerShell(tclsh90 src/make.tcl *)",
"PowerShell(& \"C:\\Users\\sleek\\AppData\\Local\\Apps\\Tcl903\\bin\\tclsh90.exe\" src/make.tcl *)",
"PowerShell(& c:\\repo\\jn\\shellspy\\bin\\punk902z.exe src/make.tcl *)",
"PowerShell(Get-Process *)",
"PowerShell(git status *)",
"PowerShell(git log *)",
"PowerShell(git diff *)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|PowerShell",
"hooks": [
{
"type": "command",
"command": "cmd=$(jq -r '.tool_input.command // empty'); if printf '%s' \"$cmd\" | grep -qiE '(^|[|;&(]|\\$\\()[[:space:]]*[^|;&[:space:]]*tclsh[0-9.]*(\\.exe)?\"?[[:space:]]+(-encoding[[:space:]]+[^[:space:]]+[[:space:]]+)?-e([[:space:]]|$)'; then printf '%s' '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"tclsh has no -e one-liner flag. An argument starting with - is not treated as a script file: all args land in $argv and tclsh reads commands from stdin instead (hangs forever at an interactive prompt on a console; silently ignores the -e script when stdin is piped). Write the code to a temp .tcl file and run: tclsh path/to/file.tcl - or pipe the script via stdin. See AGENTS.md User Preferences.\"}}'; fi",
"shell": "bash",
"timeout": 15,
"statusMessage": "Checking for tclsh -e misuse"
}
]
}
]
}
}

2
.fossil-settings/AGENTS.md

@ -24,7 +24,7 @@ Versioned fossil settings (one setting value or glob list per file) for this dua
### ignore-glob derivation rules (verified against fossil 2.28)
- **No negation exists in fossil globs.** Git's `/bin/*` + `!exception` pattern becomes: ignore `bin/*` wholesale and explicitly `fossil add` the tracked exceptions (ignore-glob never affects already-managed files, so they stay tracked). The exception set is whatever git tracks under an ignored tree - at the time of writing: `bin/AGENTS.md`, `bin/*.cmd`, `bin/*.kit`, `bin/*.tcl`, `bin/*.sh`, `bin/*.bash`, plus force-tracked one-offs (`bin/libssp-0.dll`, `src/vfs/punk9magicsplat.vfs/lib/nmake/x86_64-w64-mingw32-nmakehlp.exe`). Derive the current set with the verification comparison below rather than trusting this list.
- **No negation exists in fossil globs.** Git's `/bin/*` + `!exception` pattern becomes: ignore `bin/*` wholesale and explicitly `fossil add` the tracked exceptions (ignore-glob never affects already-managed files, so they stay tracked). The exception set is whatever git tracks under an ignored tree - at the time of writing: `bin/AGENTS.md`, `bin/*.cmd`, `bin/*.kit`, `bin/*.tcl`, `bin/*.sh`, `bin/*.bash`, `.claude/settings.json` (shared claude harness hooks/permissions; session-local `.claude` files stay ignored), plus force-tracked one-offs (`bin/libssp-0.dll`, `src/vfs/punk9magicsplat.vfs/lib/nmake/x86_64-w64-mingw32-nmakehlp.exe`). Derive the current set with the verification comparison below rather than trusting this list.
- A bare directory name prunes that whole tree; `*` crosses `/`; git patterns intended to match at any depth need an additional `*/` variant; `#` comment lines are honoured.
- Do not add the fossil-generated names (`manifest`, `manifest.uuid`, `manifest.tags`) to `ignore-glob` - fossil handles them itself.
- **Nested `.gitignore` files are git-only**: ignore files inside subtrees (e.g. the project-layout templates under `src/project_layouts/`) are honoured by git as nested ignores of the outer repo but invisible to fossil - template content they match is untracked in git yet managed by fossil. Treat such divergence as a signal to review the git side (usually `git add -f` of the affected template files).

3
.fossil-settings/ignore-glob

@ -22,7 +22,8 @@ tmp
.vscode
*/.vscode
.claude
#claude harness config: shared .claude/settings.json is a tracked exception by explicit add (see header)
.claude/*
*/.claude
.omo
*/.omo

4
.gitignore vendored

@ -23,7 +23,9 @@
/tmp/
.vscode
.claude
#claude harness config: track the shared project settings (hooks/permissions), ignore everything else (session-local settings etc)
.claude/*
!.claude/settings.json
.omo
/logs/

1
AGENTS.md

@ -95,6 +95,7 @@ When the user requests a durable behavior change, record it here or in the relev
- If the active editor is on a source-derived snapshot, bootstrap copy, or build output path such as `src/bootsupport/`, root `modules/`, root `lib/`, `modules_tcl8/`, `modules_tcl9/`, `lib_tcl8/`, or `lib_tcl9/`, confirm the intended target before editing unless the user explicitly named that path.
- cmd.exe PATH truncation (this machine, and any Windows machine with a heavily populated PATH): cmd.exe truncates a long PATH, so a tool that resolves fine in PowerShell may be "not found" when invoked via `cmd.exe /c`. Use absolute executable paths inside any `cmd /c` command line, and prefer PowerShell-native invocation unless a console host is specifically required (e.g. hidden-console test harnesses). If a tool is missing only under cmd.exe, suspect truncation before absence.
- Agent-authored text is plain ASCII by default: no em/en dashes, curly quotes, arrow or ellipsis characters, or other typographic Unicode - use ASCII equivalents (" - ", straight quotes, "->", "..."). This applies with extra force to outward-bound artifacts (ticket drafts, bug reports, emails, commit messages, anything likely to be pasted into an external system): those must be pure ASCII, verified before handover (e.g. grep for `[^\x00-\x7F]`). Legitimate exceptions: content whose subject matter is itself non-ASCII (encoding/Unicode/ANSI-art test data, or documentation demonstrating such behaviour), verbatim quotes of existing material, and cases where the user explicitly requests non-ASCII. Existing files are not to be bulk-retrofitted - the rule governs newly written text.
- Tcl has no `-e`/`-c` one-liner flag (a reflex agents carry over from perl/python/node). Stock `tclsh`/`tclsh86`/`tclsh90` recognise only `-encoding name` as a leading option; any other argument starting with `-` is NOT treated as a script file - all arguments land in `$argv` and tclsh reads commands from stdin. On a console that hangs forever at an interactive prompt; with piped/redirected stdin it exits 0 having silently ignored the supposed one-liner and executed stdin instead. (The punk kits differ: they treat `-e` as a script filename and error out immediately - no hang, but still no one-liner.) To run ad-hoc Tcl: write a temp `.tcl` file and run `tclsh path/to/file.tcl`, or pipe the script to stdin (`echo 'puts hi' | tclsh`, or a bash heredoc). Defensive habit regardless: when exec'ing tclsh non-interactively, redirect stdin (`</dev/null`, `< NUL`) so a mis-invocation exits at EOF instead of hanging.
- Throwaway fossil repositories (test/experiment repos an agent creates, e.g. in a session scratchpad) must not register in the user's real global fossil config-db: `fossil init`/`fossil open` write persistent `repo:`/`ckout:` rows into `%LOCALAPPDATA%\_fossil`, which is the enumeration source for `dev projects.work` project discovery (G-016/G-017). Set `FOSSIL_HOME` to a disposable scratch directory for the duration of such fossil commands (both fossil and `punk::repo::fossil_get_configdb` honour it first). If pollution has already occurred: `fossil all ignore <repo-path>`, delete the directory, then any `fossil all` command prunes the orphaned `ckout:` row.
- Do not commit new executable binaries (shared libs, .exe, native .so/.dll/.dylib, bare ELF/Mach-O, or zip-based .tm modules embedding executables) to the repository. Existing binaries in `bin/`, `src/vfs/`, `src/vendorlib/`, `src/vendormodules/`, and `src/bootsupport/` are there intentionally pending the build/retrieval infrastructure tracked by goals G-004/G-005/G-006; do not flag, "fix", or hassle the developer about these — they are known and will be removed once G-005 (zig build) or G-006 (pre-built download) provides an alternative. This rule stops agents from adding new binaries; it does not block the developer's interim commits of existing vendor/vfs binaries. It is workflow policy only - deliberately NOT enforced at the local VCS layer: fossil `binary-glob` is `*` (versioned in `.fossil-settings/`) so binary checkins proceed without warnings/prompts, here and in sub-projects like tomlish.

Loading…
Cancel
Save