# How I'm still using my Claude subscription for unattended jobs

Gustavo Ambrozio 10 min read
Table of Contents

Recently Anthropic changed their policy to make using claude -p (the headless / print mode that almost every Claude Code automation I’ve written depends on) draw from API credits instead of my subscription usage limits. Interactive Claude Code stays on the subscription I already pay for; only -p gets the per-token bill. Policy change Needless to say, despite their attempt to make this sound like a good thing, I was not very happy with this change and was not about to give up my VC-backed subsidized tokens just yet. So I started thinking about how I could keep all my automations while still not having to pay this new -p tax.

I was pretty sure I could accomplish the same as -p with an “interactive” session, a few hooks, and some tmux trickery.

After a few attempts I had a Claude Code plugin and a couple of wrapper scripts that drive a normal interactive Claude session as if there were a human at the keyboard.

TL;DR: How can you use this

If all you need is to replace your use of claude -p, then the claude-auto script can probably replace it with no changes. Just download it (and claude-transcript), put them somewhere in your $PATH, and use claude-auto wherever you used claude -p.

For example, one of my scripts was just claude -p --dangerously-skip-permissions "/pr-comment". pr-comment is one of my skills that runs a code review on the current GitHub PR for the branch it is running on and adds comments to it. For this one I replaced it with claude-auto --dangerously-skip-permissions "/pr-comment".

The output changes a bit but, IMHO, in a good way. claude-auto gives you a simple transcript of what Claude did during your session in a way that was hard to get with claude -p. This comes from claude-transcript.

Look at the examples folder for sample scripts - cron, batch, parallel-across-repos, and an attended variant for the rare cases where you do want Claude to be able to ask you a question.

How does it work

A Claude Code plugin is a directory with a collection of hooks, skills, agents, etc. Autonomy is four small hook handlers.

SessionStart: tell Claude to act without user input

One of the main uses of a SessionStart hook is to inject context at the start of a session. Autonomy injects a short prompt to steer Claude into working without user input. By default it assumes nobody is watching:

You are running in fully unattended mode. There is no human available to
answer questions, confirm decisions, or approve plans.
Do NOT use the `AskUserQuestion` tool — it is blocked in this mode and
will return a stock denial telling you to proceed without asking. Do not
enter plan mode to seek approval, and do not stop to request clarification.
When requirements are ambiguous, make a reasonable judgement call based on
the existing code, project conventions, and the task description, then
continue. State any non-obvious assumptions you made in your final output
so they can be reviewed after the session ends.

If you do plan to attach to the tmux session and want to answer the occasional question, set CLAUDE_AUTO_QUESTIONS_OK=1 and Autonomy injects the softer version instead:

You should work as autonomously as possible on this session so there's no
need to get into plan mode to create a plan and ask the user for approval.
If you deem that the work cannot be done without user input then you can
ask questions. Use the `AskUserQuestion` tool for this. But only do this
if it is absolutely necessary, otherwise just do the work without user
input.

PreToolUse: actually blocking questions

The SessionStart prompt is just a hint. Claude sometimes decides this is the absolutely-necessary case and reaches for AskUserQuestion anyway. In an unattended run “you should not ask” needs to become “you cannot ask”, otherwise the session sits there forever waiting for an answer that will never come.

The fix is a PreToolUse hook with "matcher": "AskUserQuestion". When the hook prints this JSON to stdout, Claude Code refuses to run the tool and surfaces permissionDecisionReason to the model in place of a result:

{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "This session is running in autonomous mode, so no human is available to answer. Use your best judgement based on the existing code and the original task description, and continue. State any non-obvious assumptions in your final output for later review."
}
}

The hook reads CLAUDE_AUTO_QUESTIONS_OK from the environment. If it’s 1, the script exits 0 with no output (allow); otherwise it prints the deny JSON. The same env var is what swaps the SessionStart prompt between the two variants above, so the model and the enforcement layer stay consistent.

The default is strict. Autonomy assumes nobody is watching. If you intend to attach to the tmux session and want to be pinged for ambiguous calls, CLAUDE_AUTO_QUESTIONS_OK=1 claude-auto "..." flips both layers to the soft version. examples/attended-run.sh is a thin wrapper that sets it for you.

Stop: when stop doesn’t always mean stop

The first version of this hook was dead simple: tmux send-keys /exit. Every time Stop fired, the session ended because I naively thought that Stop meant the agent was done working. Not really.

In many situations where Claude needs to wait for some long-running task, it can start a background task or a subagent to do that. In these cases it does fire a Stop hook, but it says it is waiting for something to finish. When that happened, my script would “type” /exit and the agent would ask for confirmation before exiting, but there was nothing to confirm or deny, so the flow just broke.

So what I did instead of just exiting was to ask Claude. My Stop hook would type a question: “If there are no background jobs you’re waiting for and you’re done working reply with ‘done’ only. If you are waiting for background jobs reply with ‘waiting’ only.”

The Stop hook input JSON includes a last_assistant_message field with the text of Claude’s final reply for that turn. The hook reads it and routes on its content:

  • "done" → really done, send /exit to end the session.
  • "waiting" → tool or subagent still in flight; do nothing, the agent will trigger a new Stop when whatever it’s running ends.
  • anything else → ask again.

The first time a session stops, last_assistant_message is whatever Claude was saying (usually a summary of work) so we nudge. Claude reads the nudge, classifies its own state, replies with a single word. That reply triggers another Stop. That second Stop’s last_assistant_message is "done" or "waiting", and the hook acts on it.

StopFailure: retry with a counter

Just as I was testing the script, Claude started having API issues and returning a bunch of server errors. Kinda lucky in a weird way, as it exposed a potential issue with this approach.

StopFailure is fired when a turn ends because of an API error. We can’t tell the difference between recoverable and non-recoverable failures from inside the hook, so we retry up to a limit and then give up.

The handler keeps a counter file at $TMPDIR/<session-id>. On each failure it increments. For the first five failures it types Up + Enter into the tmux pane after a ten-second pause. Up recalls the previous prompt in Claude’s history and Enter re-submits it. On the sixth, it removes the counter and types /exit. Five retries with a 10s baseline have been enough to ride out every transient I’ve hit; if you have a permanently broken auth or billing setup, you’ll burn through five retries and then exit, which is probably better than what claude -p would do in this situation.

claude-auto

Plugins are supposed to be installed in your agent’s configuration, but Autonomy is different: it is only needed in targeted situations. claude-auto handles that, plus a few other useful things.

claude-auto does four things:

  1. Generates a fresh UUID for --session-id, so the transcript can be retrieved later.
  2. Loads the plugin via --plugin-url https://github.com/gpambrozio/Autonomy/archive/refs/heads/main.zip so there’s no install step for the user. Claude Code fetches the zip on the fly.
  3. Routes stdin/stdout/stderr so that the session renders on the terminal even when the caller piped us into tee.
  4. Also, after the session ends it pipes the whole thing through claude-transcript so you have something to scroll back through (or, with --log <file>, written to a file).

claude-auto itself doesn’t know about CLAUDE_AUTO_QUESTIONS_OK. It just doesn’t strip the environment, so whatever you export reaches the hooks.

The typical invocation is:

Terminal window
claude-auto "do a thing"

You can also add any other flags you’d pass to claude:

Terminal window
claude-auto --dangerously-skip-permissions --effort max --model opus "/github-issue $1"

Getting Claude Code to render when stdout is piped

This was a bit tricky. In some of my scripts I was capturing the output of claude -p into a log file, so when I replaced it with claude-auto I did this:

Terminal window
claude-auto "..." | tee log.txt

Now Claude’s stdout is the pipe to tee, not a TTY. When Claude detects that stdout isn’t a TTY, it falls back to plain-text non-interactive output, which is the same as -p and would probably charge you accordingly.

Ironically, I used Claude to figure out how to get itself to still behave as if it were in a TTY even though I was piping stdout.

As with many shell tricks Claude knows, I don’t fully understand how this works. Claude’s explanation is this:

In the realistic call shapes, stdin is still a TTY even when stdout is piped. The shell only redirects what the pipe captures. If you run cmd | tee log, the pipe attaches to cmd’s stdout; the parent shell’s stdin (and cmd’s stdin, by inheritance) is still your terminal. So I just dup the inherited stdin into stdout and stderr:

Terminal window
if [ -t 0 ] && [ -t 1 ] && [ -t 2 ]; then
claude ... "$@" # nothing to fix
elif [ -t 0 ]; then
claude ... "$@" 1>&0 2>&0 # stdin is a TTY, dup it into the others
else
claude ... "$@" # no TTY anywhere, let claude decide
fi

1>&0 2>&0 makes fd 1 and fd 2 dups of fd 0. They share the same open file description as the inherited stdin, which is a normal pty fd opened O_RDWR by the shell. Bun is happy, the TUI renders on the actual terminal, the pipe still gets whatever claude-auto writes to stdout after Claude exits, which is the transcript dump.

claude-transcript

I wanted a way to get a transcript of the session afterwards, just in case something went wrong or to be able to see what it did.

There are many projects out there that do that, but I had Claude whip up a simple script.

claude-transcript is about 100 lines of Python that walks the JSONL file Claude saves for every session and prints a chronological list of just the parts I care about:

## [1] 2026-05-14 09:12:03 — USER
Implement the new caching layer described in issue #42.
## [2] 2026-05-14 09:12:18 — CLAUDE
I'll start by reading the existing cache implementation and the issue
description to understand the requirements.
## [3] 2026-05-14 09:15:47 — CLAUDE
Implementation done. Tests pass. PR opened at #167.

claude-auto calls it on every exit, so every session ends with a self-contained log on stdout (or in the file you passed to --log).

Limitations / caveats

  • tmux required. The whole mechanism is tmux send-keys.
  • StopFailure retries everything. It can’t distinguish a 429 from a 401, so a session with broken auth will retry five times before exiting.
  • No questions by default. Unattended runs deny AskUserQuestion at the PreToolUse hook. If your automation depends on a human being there to answer, set CLAUDE_AUTO_QUESTIONS_OK=1 and attach to the tmux session.
  • It’s a workaround. Anthropic could try to detect tricks like this and treat them as -p. My guess is that all these subscriptions will cease to exist or start to get severely limited anyway.

Try it

  • Plugin repo: github.com/gpambrozio/Autonomy
  • Drop bin/claude-auto and bin/claude-transcript on your $PATH, install tmux, start a tmux session, and run claude-auto "do a thing". The plugin is fetched from GitHub on every invocation; there’s no install step beyond that.
My avatar

Thanks for reading! Feel free to contact me via the social links in the footer.


More Posts

Comments